import { effect, signal, type Cleanup, type ReadonlySignal } from "./signal.ts";

export interface WatchOptions<T> {
  immediate?: boolean;
  equals?: (left: T, right: T) => boolean;
}

export function watch<T>(
  read: () => T,
  listener: (value: T, previous: T | undefined) => void | Cleanup,
  options: WatchOptions<T> = {},
): Cleanup {
  const equals = options.equals ?? Object.is;
  let initialized = false;
  let previous: T | undefined;
  let listenerCleanup: void | Cleanup;
  const dispose = effect(() => {
    const value = read();
    const old = previous;
    if (!initialized) {
      initialized = true;
      if (!options.immediate) {
        previous = value;
        return;
      }
    } else if (equals(value, previous as T)) {
      return;
    }
    if (typeof listenerCleanup === "function") listenerCleanup();
    previous = value;
    listenerCleanup = listener(value, old);
  });
  return () => {
    dispose();
    if (typeof listenerCleanup === "function") listenerCleanup();
  };
}

export type ResourceStatus = "idle" | "pending" | "success" | "error";
export interface Resource<T> {
  data: ReadonlySignal<T | undefined>;
  error: ReadonlySignal<unknown>;
  status: ReadonlySignal<ResourceStatus>;
  loading: ReadonlySignal<boolean>;
  run(): Promise<T | undefined>;
  abort(reason?: unknown): void;
  reset(): void;
}

export interface ResourceOptions<T> {
  initial?: T;
  immediate?: boolean;
  keepPrevious?: boolean;
}

export function resource<T>(
  loader: (signal: AbortSignal) => Promise<T>,
  options: ResourceOptions<T> = {},
): Resource<T> {
  const data = signal<T | undefined>(options.initial);
  const error = signal<unknown>(undefined);
  const status = signal<ResourceStatus>("idle");
  let controller: AbortController | null = null;
  let generation = 0;

  const api: Resource<T> = {
    data,
    error,
    status,
    loading: {
      get: () => status.get() === "pending",
      subscribe(fn) {
        return status.subscribe((value, previous) =>
          fn(value === "pending", previous === "pending"),
        );
      },
    },
    async run() {
      controller?.abort("superseded");
      const requestController = new AbortController();
      controller = requestController;
      const current = ++generation;
      if (!options.keepPrevious) data.set(options.initial);
      error.set(undefined);
      status.set("pending");
      try {
        const value = await loader(requestController.signal);
        if (current !== generation || requestController.signal.aborted) {
          return undefined;
        }
        controller = null;
        data.set(value);
        status.set("success");
        return value;
      } catch (cause) {
        if (current !== generation || requestController.signal.aborted) {
          return undefined;
        }
        controller = null;
        error.set(cause);
        status.set("error");
        return undefined;
      }
    },
    abort(reason) {
      generation++;
      controller?.abort(reason);
      controller = null;
      if (status.get() === "pending") status.set("idle");
    },
    reset() {
      api.abort("reset");
      data.set(options.initial);
      error.set(undefined);
      status.set("idle");
    },
  };
  if (options.immediate) void api.run();
  return api;
}

export interface ReactiveScope {
  add(cleanup: Cleanup): Cleanup;
  dispose(): void;
  readonly disposed: boolean;
}

export function createScope(): ReactiveScope {
  const cleanups = new Set<Cleanup>();
  let disposed = false;
  return {
    add(cleanup) {
      if (disposed) {
        cleanup();
        return cleanup;
      }
      cleanups.add(cleanup);
      return () => {
        if (cleanups.delete(cleanup)) cleanup();
      };
    },
    dispose() {
      if (disposed) return;
      disposed = true;
      for (const cleanup of [...cleanups].reverse()) cleanup();
      cleanups.clear();
    },
    get disposed() {
      return disposed;
    },
  };
}
