import { signal, type Signal } from "./signal.ts";

export interface HistorySignal<T> extends Signal<T> {
  undo(): boolean;
  redo(): boolean;
  canUndo(): boolean;
  canRedo(): boolean;
  clearHistory(): void;
}
export function historySignal<T>(
  initial: T,
  options: { limit?: number; equals?: (left: T, right: T) => boolean } = {},
): HistorySignal<T> {
  const limit = options.limit ?? 100;
  if (!Number.isInteger(limit) || limit < 1) throw new RangeError("history limit must be positive");
  const current = signal(initial);
  const past: T[] = [];
  const future: T[] = [];
  const equals = options.equals ?? Object.is;
  const set = current.set.bind(current);
  current.set = (value) => {
    const next = typeof value === "function" ? (value as (previous: T) => T)(current.get()) : value;
    const previous = current.get();
    if (equals(previous, next)) return;
    past.push(previous);
    if (past.length > limit) past.shift();
    future.length = 0;
    set(next);
  };
  return Object.assign(current, {
    undo() {
      const value = past.pop();
      if (value === undefined) return false;
      future.push(current.get());
      set(value);
      return true;
    },
    redo() {
      const value = future.pop();
      if (value === undefined) return false;
      past.push(current.get());
      set(value);
      return true;
    },
    canUndo: () => past.length > 0,
    canRedo: () => future.length > 0,
    clearHistory() {
      past.length = 0;
      future.length = 0;
    },
  });
}

export interface UrlStateOptions<T> {
  url?: URL;
  parameter: string;
  parse?: (value: string | null) => T;
  serialize?: (value: T) => string | null;
  replace?: (url: URL) => void;
}
export function urlSignal<T>(initial: T, options: UrlStateOptions<T>): Signal<T> {
  const url = options.url ?? new URL(globalThis.location?.href ?? "http://localhost/");
  const parsed = options.parse?.(url.searchParams.get(options.parameter));
  const state = signal(parsed ?? initial);
  const original = state.set.bind(state);
  state.set = (value) => {
    original(value);
    const serialized = options.serialize ? options.serialize(state.get()) : String(state.get());
    if (serialized === null) url.searchParams.delete(options.parameter);
    else url.searchParams.set(options.parameter, serialized);
    if (options.replace) options.replace(new URL(url));
    else globalThis.history?.replaceState?.(null, "", url);
  };
  return state;
}

export interface ReactiveContext<T> {
  provide<R>(value: T, run: () => R): R;
  use(): T;
}
export function createContextProvider<T>(defaultValue?: T): ReactiveContext<T> {
  const stack: T[] = [];
  return {
    provide(value, run) {
      stack.push(value);
      try {
        return run();
      } finally {
        stack.pop();
      }
    },
    use() {
      if (stack.length) return stack[stack.length - 1]!;
      if (defaultValue !== undefined) return defaultValue;
      throw new Error("WRN-REACTIVE-CONTEXT: no provider is active.");
    },
  };
}

export function mountPortal(content: Node | string, target: Element): () => void {
  const marker = target.ownerDocument.createComment("wrnexus-portal");
  const node = typeof content === "string" ? target.ownerDocument.createTextNode(content) : content;
  target.append(marker, node);
  return () => {
    marker.remove();
    node.parentNode?.removeChild(node);
  };
}

export async function transition(
  update: () => void,
  options: { className?: string; target?: Element; durationMs?: number; signal?: AbortSignal } = {},
): Promise<void> {
  if (options.signal?.aborted) throw options.signal.reason;
  const target = options.target;
  const className = options.className ?? "wrn-transition";
  target?.classList.add(className);
  update();
  const duration = options.durationMs ?? 0;
  if (duration > 0)
    await new Promise<void>((resolve, reject) => {
      const timer = setTimeout(resolve, duration);
      options.signal?.addEventListener(
        "abort",
        () => {
          clearTimeout(timer);
          reject(options.signal!.reason);
        },
        { once: true },
      );
    });
  target?.classList.remove(className);
}

export interface TimelineStep {
  durationMs: number;
  delayMs?: number;
  easing?: (progress: number) => number;
  update(progress: number): void;
}
export interface AnimationTimeline {
  play(options?: { reverse?: boolean; signal?: AbortSignal }): Promise<void>;
  cancel(reason?: unknown): void;
  readonly running: boolean;
}

export function createTimeline(
  steps: TimelineStep[],
  options: { now?: () => number; frame?: (callback: () => void) => unknown } = {},
): AnimationTimeline {
  for (const step of steps) {
    if (
      !Number.isFinite(step.durationMs) ||
      step.durationMs < 0 ||
      !Number.isFinite(step.delayMs ?? 0) ||
      (step.delayMs ?? 0) < 0
    )
      throw new RangeError("Timeline durations and delays must be non-negative");
  }
  const now = options.now ?? (() => performance.now());
  const frame = options.frame ?? ((callback) => requestAnimationFrame(callback));
  let controller: AbortController | null = null;
  const timeline: AnimationTimeline = {
    get running() {
      return controller !== null;
    },
    cancel(reason = new Error("Timeline cancelled")) {
      controller?.abort(reason);
    },
    async play(playOptions = {}) {
      controller?.abort(new Error("Timeline restarted"));
      controller = new AbortController();
      const local = controller;
      playOptions.signal?.addEventListener("abort", () => local.abort(playOptions.signal!.reason), {
        once: true,
      });
      const ordered = playOptions.reverse ? [...steps].reverse() : steps;
      try {
        for (const step of ordered) {
          const delay = step.delayMs ?? 0;
          if (delay)
            await new Promise<void>((resolve, reject) => {
              const timer = setTimeout(resolve, delay);
              local.signal.addEventListener(
                "abort",
                () => {
                  clearTimeout(timer);
                  reject(local.signal.reason);
                },
                { once: true },
              );
            });
          const started = now();
          await new Promise<void>((resolve, reject) => {
            const tick = () => {
              if (local.signal.aborted) {
                reject(local.signal.reason);
                return;
              }
              const raw =
                step.durationMs === 0 ? 1 : Math.min(1, (now() - started) / step.durationMs);
              const progress = playOptions.reverse ? 1 - raw : raw;
              step.update(step.easing?.(progress) ?? progress);
              if (raw >= 1) resolve();
              else frame(tick);
            };
            tick();
          });
        }
      } finally {
        if (controller === local) controller = null;
      }
    },
  };
  return timeline;
}
