import { createSignal, getListener, onCleanup, type SignalOptions, DEV } from "solid-js";
import { isServer } from "solid-js/web";

const noop = () => {};

const triggerOptions: SignalOptions<any> =
  !isServer && DEV ? { equals: false, name: "trigger" } : { equals: false };

const triggerCacheOptions: SignalOptions<any> =
  !isServer && DEV ? { equals: false, internal: true } : triggerOptions;

export type Trigger = [track: VoidFunction, dirty: VoidFunction];

/**
 * Set listeners in reactive computations and then trigger them when you want.
 * @returns `[track function, dirty function]`
 * @example
 * const [track, dirty] = createTrigger()
 * createEffect(() => {
 *    track()
 *    ...
 * })
 * // later
 * dirty()
 */
export function createTrigger(): Trigger {
  if (isServer) {
    return [noop, noop];
  }
  return createSignal(undefined, triggerOptions);
}

export class TriggerCache<T> {
  #map: Map<
    T,
    {
      // track
      $(): void;
      // dirty
      $$(): void;
      // n of listeners
      n: number;
    }
  >;

  constructor(mapConstructor: WeakMapConstructor | MapConstructor = Map) {
    this.#map = new (mapConstructor as MapConstructor)();
  }

  dirty(key: T) {
    if (isServer) return;
    this.#map.get(key)?.$$();
  }

  dirtyAll() {
    if (isServer) return;
    for (const trigger of this.#map.values()) trigger.$$();
  }

  track(key: T) {
    if (!getListener()) return;
    let trigger = this.#map.get(key);
    if (!trigger) {
      const [$, $$] = createSignal(undefined, triggerCacheOptions);
      this.#map.set(key, (trigger = { $, $$, n: 1 }));
    } else trigger.n++;
    onCleanup(() => {
      // remove the trigger when no one is listening to it
      if (--trigger.n === 0)
        // microtask is to avoid removing the trigger used by a single listener
        queueMicrotask(() => trigger.n === 0 && this.#map.delete(key));
    });
    trigger.$();
  }
}