import { batchFlush, batchStart, type BatchTask, batchTasks } from "../batch";
import { context } from "../context";
import { type EventObject, on, type RemoveListener, send, size } from "../event";
import { type ReadableProvider, type OwnedWritable, type Readable } from "../interface";
import { writable } from "../readable";
import { onDisposeValue, type OnDisposeValue } from "./utils";

export interface ReactiveSetChanged<V> {
  readonly upsert: readonly V[];
  readonly delete: readonly V[];
}

interface OnChanged<V> extends BatchTask<EventObject<ReactiveSetChanged<V>>> {
  readonly upsert_: Set<V>;
  readonly delete_: Set<V>;
}

/**
 * OwnedReactiveSet extends the standard Set interface with reactive capabilities.
 *
 * @category ReactiveSet
 */
export class OwnedReactiveSet<V> extends Set<V> implements ReadableProvider<ReadonlyReactiveSet<V>> {
  /**
   * A Readable that emits the Set itself whenever it changes.
   *
   * @group Readable
   */
  public get $(): Readable<ReadonlyReactiveSet<V>> {
    return (this._$ ??= writable(this, { equal: false }));
  }

  /**
   * Subscribe to changes in the set.
   *
   * @group Events
   * @param fn - The function to call when the set is changed.
   * @returns A disposer function to unsubscribe from the event.
   *
   * @example
   * ```ts
   * import { reactiveSet } from "@embra/reactivity";
   *
   * const set = reactiveSet<number>();
   * const disposer = set.onChanged((changed) => {
   *   console.log("Set changed:", changed);
   * });
   * ```
   */
  public onChanged(fn: (changed: ReactiveSetChanged<V>) => void): RemoveListener {
    return on(
      (this._onChanged_ ??= {
        delete_: new Set<V>(),
        upsert_: new Set<V>(),
        batchTask_: () => {
          if (this._onChanged_ && size(this._onChanged_)) {
            const { upsert_, delete_ } = this._onChanged_;
            /** c8 ignore else -- @preserve */
            if (upsert_.size > 0 || delete_.size > 0) {
              const changedData = {
                upsert: [...upsert_],
                delete: [...delete_],
              };
              upsert_.clear();
              delete_.clear();
              send(this._onChanged_, changedData);
            }
          } else {
            this._onChanged_ = null;
          }
        },
      }),
      fn,
    );
  }

  /**
   * Subscribe to events when a value is needed to be disposed.
   *
   * A value is considered for disposal when:
   * - it is deleted from the set.
   * - it is replaced by another value (the old value is removed).
   * - it is cleared from the set.
   * - the set is disposed.
   *
   * @function
   * @group Events
   * @param fn - The function to call when a value is needed to be disposed.
   * @returns A disposer function to unsubscribe from the event.
   *
   * @example
   * ```ts
   * import { reactiveSet } from "@embra/reactivity";
   *
   * const set = reactiveSet<number>();
   * const disposer = set.onDisposeValue((value) => {
   *   console.log("Value disposed:", value);
   * });
   * ```
   */
  public readonly onDisposeValue = onDisposeValue;

  public constructor(values?: Iterable<V> | null) {
    super();

    if (values) {
      for (const value of values) {
        super.add(value);
      }
    }
  }

  public dispose(): void {
    if (this._disposed_) return;
    if (process.env.NODE_ENV !== "production") {
      this._disposed_ = new Error("[embra] ReactiveSet disposed at:");
    } else {
      this._disposed_ = true;
    }
    if (this.onDisposeValue_) {
      const { delete_ } = this.onDisposeValue_;
      for (const value of this.values()) {
        delete_.add(value);
      }
      if (delete_.size) {
        const isBatchTop = batchStart();
        batchTasks.add(this.onDisposeValue_);
        isBatchTop && batchFlush();
      }
    }
    this._$ = this._onChanged_ = this.onDisposeValue_ = null;
  }

  public override add(value: V): this {
    if (!this.has(value)) {
      const isBatchTop = batchStart();
      this.onDisposeValue_?.delete_.delete(value);
      if (this._onChanged_) {
        this._onChanged_.upsert_.add(value);
        this._onChanged_.delete_.delete(value);
        batchTasks.add(this._onChanged_);
      }
      super.add(value);
      this._notify_();
      isBatchTop && batchFlush();
    }
    return this;
  }

  public override delete(value: V): boolean {
    if (this.has(value)) {
      const isBatchTop = batchStart();
      if (this.onDisposeValue_) {
        this.onDisposeValue_.delete_.add(value);
        batchTasks.add(this.onDisposeValue_);
      }
      if (this._onChanged_) {
        this._onChanged_.delete_.add(value);
        this._onChanged_.upsert_.delete(value);
        batchTasks.add(this._onChanged_);
      }
      this._notify_();
      isBatchTop && batchFlush();
    }
    return super.delete(value);
  }

  public override clear(): void {
    if (this.size) {
      const isBatchTop = batchStart();
      if (this.onDisposeValue_) {
        for (const value of this) {
          this.onDisposeValue_.delete_.add(value);
          batchTasks.add(this.onDisposeValue_);
        }
      }
      if (this._onChanged_) {
        for (const value of this) {
          this._onChanged_.delete_.add(value);
          this._onChanged_.upsert_.delete(value);
          batchTasks.add(this._onChanged_);
        }
      }
      super.clear();
      this._notify_();
      isBatchTop && batchFlush();
    }
  }

  /**
   * Replace the contents of the set with the given values.
   * @param values - The new values to replace the set with.
   * @returns The set itself.
   */
  public replace(values: Iterable<V>): this {
    const isBatchTop = batchStart();
    const newValues: Set<V> = (context.replaceMarkers_ ??= new Set());
    for (const value of values) {
      newValues.add(value);
      this.add(value);
    }
    for (const value of this) {
      if (!newValues.has(value)) {
        this.delete(value);
      }
    }
    newValues.clear();
    isBatchTop && batchFlush();
    return this;
  }

  /** @internal */
  private _disposed_?: Error | true;

  /** @internal */
  private _$?: OwnedWritable<this> | null;

  /** @internal */
  private _onChanged_?: null | OnChanged<V>;

  /** @internal */
  public onDisposeValue_?: null | OnDisposeValue<V>;

  /** @internal */
  private _notify_() {
    if (this._disposed_) {
      console.error(this, new Error("disposed"));
      if (process.env.NODE_ENV !== "production") {
        console.error(this._disposed_);
      }
    }
    this._$?.set(this);
  }
}

/**
 * ReactiveSet is {@link OwnedReactiveSet} without the `dispose` method.
 *
 * @category ReactiveSet
 */
export type ReactiveSet<V> = Omit<OwnedReactiveSet<V>, "dispose">;

/**
 * ReadonlyReactiveSet is a readonly interface for {@link ReactiveSet}.
 *
 * @category ReactiveSet
 */
export interface ReadonlyReactiveSet<V> extends ReadonlySet<V> {
  /**
   * A Readable that emits the Set itself whenever it changes.
   *
   * @group Readable
   */
  readonly $: Readable<ReadonlySet<V>>;
  /**
   * Subscribe to changes in the set.
   *
   * @group Events
   * @param fn - The function to call when the set is changed.
   * @returns A disposer function to unsubscribe from the event.
   *
   * @example
   * ```ts
   * import { reactiveSet } from "@embra/reactivity";
   *
   * const set = reactiveSet<number>();
   * const disposer = set.onChanged((changed) => {
   *   console.log("Set changed:", changed);
   * });
   * ```
   */
  onChanged(fn: (changed: ReactiveSetChanged<V>) => void): RemoveListener;
  /**
   * Subscribe to events when a value is needed to be disposed.
   *
   * A value is considered for disposal when:
   * - it is deleted from the set.
   * - it is replaced by another value (the old value is removed).
   * - it is cleared from the set.
   * - the set is disposed.
   *
   * @function
   * @group Events
   * @param fn - The function to call when a value is needed to be disposed.
   * @returns A disposer function to unsubscribe from the event.
   *
   * @example
   * ```ts
   * import { reactiveSet } from "@embra/reactivity";
   *
   * const set = reactiveSet<number>();
   * const disposer = set.onDisposeValue((value) => {
   *   console.log("Value disposed:", value);
   * });
   * ```
   */
  readonly onDisposeValue: (fn: (value: V) => void) => RemoveListener;
}

/**
 * Creates a new {@link OwnedReactiveSet}.
 *
 * @category ReactiveSet
 * @param values - Initial values for the reactive set.
 * @returns A new instance of {@link OwnedReactiveSet}.
 *
 * @example
 * ```ts
 * import { reactiveSet } from "@embra/reactivity";
 *
 * const set$ = reactiveSet([1, 2, 3]);
 * ```
 */
export const reactiveSet = <V>(values?: Iterable<V> | null): OwnedReactiveSet<V> => new OwnedReactiveSet(values);
