import { DeepImmutable, DeepRequired } from './type';
import { isFinite, ObjectAssign } from './alias';
import { clock } from './chrono';
import { Coroutine, ICoroutine, isCoroutine } from './coroutine';
import { Observation, Observer, Publisher } from './observer';
import { AtomicPromise } from './promise';
import { AtomicFuture } from './future';
import { noop } from './function';
import { Ring } from './ring';
import { causeAsyncException } from './exception';

export interface SupervisorOptions {
  readonly name?: string;
  readonly capacity?: number;
  readonly timeout?: number;
  readonly destructor?: (reason: unknown) => void;
  readonly scheduler?: (cb: () => void) => void;
  readonly resource?: number;
}

export interface Supervisor<N extends string, P, R, S> extends Coroutine<undefined, undefined, undefined> {
  // #36218
  constructor: typeof Supervisor & typeof Coroutine;
}
export abstract class Supervisor<N extends string, P = undefined, R = P, S = undefined> extends Coroutine<undefined, undefined, undefined> {
  private static $instances: Set<Supervisor<string, unknown, unknown, unknown>>;
  private static get instances() {
    return this.hasOwnProperty('$instances')
      ? this.$instances
      : this.$instances = new Set();
  }
  private static $status: {
    readonly instances: number;
    readonly processes: number;
  };
  public static get status() {
    if (this.hasOwnProperty('$status')) return this.$status;
    const { instances } = this;
    return this.$status = {
      get instances(): number {
        return instances.size;
      },
      get processes(): number {
        return [...instances]
          .reduce((acc, sv) =>
            acc + sv.workers.size
          , 0);
      },
    } as const;
  }
  public static clear(reason?: unknown): void {
    while (this.instances.size > 0) {
      for (const sv of this.instances) {
        sv.terminate(reason);
      }
    }
  }
  protected static readonly standalone = new WeakSet<Supervisor.Process.Regular<unknown, unknown, unknown>>();
  constructor(opts: SupervisorOptions = {}) {
    super(async function* (this: Supervisor<N, P, R, S>) {
      return await this.state;
    }, { delay: false });
    ObjectAssign(this.settings, opts);
    this.name = this.settings.name;
    if (this.constructor === Supervisor) throw new Error(`Spica: Supervisor: <${this.name}>: Cannot instantiate abstract classes`);
    this.constructor.instances.add(this);
  }
  private readonly state = new AtomicFuture();
  private destructor(reason: unknown): void {
    assert(this.alive === true);
    assert(this.available === true);
    this.available = false;
    this.clear(reason);
    assert(this.workers.size === 0);
    Object.freeze(this.workers);
    while (this.messages.length > 0) {
      const { 0: names, 1: param, 4: timer } = this.messages.shift()!;
      const name: N | undefined = names[Symbol.iterator]().next().value;
      timer && clearTimeout(timer);
      this.$events?.loss.emit([name], [name, param]);
    }
    assert(this.messages.length === 0);
    this.alive = false;
    // @ts-ignore #31251
    this.constructor.instances.delete(this);
    Object.freeze(this);
    assert(this.alive === false);
    assert(this.available === false);
    this.settings.destructor(reason);
    this.state.bind(reason === undefined ? undefined : AtomicPromise.reject(reason));
  }
  public readonly name: string;
  private readonly settings: DeepImmutable<DeepRequired<SupervisorOptions>> = {
    name: '',
    capacity: Infinity,
    timeout: Infinity,
    destructor: noop,
    scheduler: clock.next,
    resource: 10,
  };
  private $events?: {
    readonly init: Observation<[N], Supervisor.Event.Data.Init<N, P, R, S>, unknown>;
    readonly exit: Observation<[N], Supervisor.Event.Data.Exit<N, P, R, S>, unknown>;
    readonly loss: Observation<[N | undefined], Supervisor.Event.Data.Loss<N, P>, unknown>;
  };
  public get events(): {
    readonly init: Observer<[N], Supervisor.Event.Data.Init<N, P, R, S>, unknown>;
    readonly exit: Observer<[N], Supervisor.Event.Data.Exit<N, P, R, S>, unknown>;
    readonly loss: Observer<[N | undefined], Supervisor.Event.Data.Loss<N, P>, unknown>;
  } {
    return this.$events ??= {
      init: new Observation<[N], Supervisor.Event.Data.Init<N, P, R, S>, unknown>(),
      exit: new Observation<[N], Supervisor.Event.Data.Exit<N, P, R, S>, unknown>(),
      loss: new Observation<[N | undefined], Supervisor.Event.Data.Loss<N, P>, unknown>(),
    };
  }
  private readonly workers = new Map<N, Worker<N, P, R, S>>();
  private alive = true;
  private available = true;
  private throwErrorIfNotAvailable(): void {
    if (!this.available) throw new Error(`Spica: Supervisor: <${this.name}>: Cannot use terminated supervisors`);
  }
  // Workaround for #36053
  public register(this: Supervisor<N, P, R, undefined>, name: N, process: Supervisor.Process.Function<P, R, S>, state?: S): (reason?: unknown) => boolean;
  public register(name: N, process: Supervisor.Process.Function<P, R, S>, state: S): (reason?: unknown) => boolean;
  public register(this: Supervisor<N, P, R, undefined>, name: N, process: Supervisor.Process.GeneratorFunction<P, R, S>, state?: S): (reason?: unknown) => boolean;
  public register(name: N, process: Supervisor.Process.GeneratorFunction<P, R, S>, state: S): (reason?: unknown) => boolean;
  public register(this: Supervisor<N, P, R, undefined>, name: N, process: Supervisor.Process.AsyncGeneratorFunction<P, R, S>, state?: S): (reason?: unknown) => boolean;
  public register(name: N, process: Supervisor.Process.AsyncGeneratorFunction<P, R, S>, state: S): (reason?: unknown) => boolean;
  public register(name: N, process: Supervisor.Process.Coroutine<P, R>, state?: never): (reason?: unknown) => boolean;
  public register(name: N, process: Supervisor.Process<P, R, S>, state: S): (reason?: unknown) => boolean;
  public register(name: N, process: Supervisor.Process<P, R, S>, state?: S): (reason?: unknown) => boolean {
    state = state!;
    this.throwErrorIfNotAvailable();
    if (isCoroutine(process)) {
      const port = process[process.constructor.port] as Coroutine<R, R, P>[typeof Coroutine.port];
      const proc: Supervisor.Process.Regular<P, R, S> = {
        init: state => state,
        main: (param, state, kill) =>
          port.ask(param)
            .then(({ value: reply, done }) =>
              done && void kill() || [reply, state]),
        exit: reason => void process[process.constructor.terminate](reason),
      };
      this.constructor.standalone.add(proc);
      const kill = this.register(name, proc, state);
      process.catch(kill);
      return kill;
    }
    if (isAsyncGeneratorFunction(process)) {
      let iter: AsyncGenerator<R, R, P>;
      return this.register(
        name,
        {
          init: (state, kill) => (iter = process(state, kill), iter.next().catch(kill), state),
          main: (param, state, kill) =>
            AtomicPromise.resolve(iter.next(param))
              .then(({ value: reply, done }) =>
                done && void kill() || [reply, state]),
          exit: noop,
        },
        state);
    }
    if (isGeneratorFunction(process)) {
      let iter: Generator<R, R, P>;
      return this.register(
        name,
        {
          init: (state, kill) => (iter = process(state, kill), iter.next(), state),
          main: (param, state, kill) => {
            const { value: reply, done } = iter.next(param);
            done && kill();
            return [reply, state];
          },
          exit: noop,
        },
        state);
    }
    if (typeof process === 'function') {
      return this.register(
        name,
        {
          init: state => state,
          main: process,
          exit: noop,
        },
        state);
    }
    if (this.workers.has(name)) throw new Error(`Spica: Supervisor: <${this.name}/${name}>: Cannot register another process with tha same name`);
    this.schedule();
    const worker: Worker<N, P, R, S> = new Worker(
      name,
      process,
      state,
      this,
      () => void this.schedule(),
      this.constructor.standalone.has(process),
      this.$events,
      () => { this.workers.get(name) === worker && void this.workers.delete(name); });
    this.workers.set(name, worker);
    return worker.terminate;
  }
  public call(name: N | ((names: Iterable<N>) => Iterable<N>), param: P, timeout?: number): AtomicPromise<R>;
  public call(name: N | ((names: Iterable<N>) => Iterable<N>), param: P, callback: Supervisor.Callback<R>, timeout?: number): void;
  public call(name: N | ((names: Iterable<N>) => Iterable<N>), param: P, callback?: Supervisor.Callback<R> | number, timeout = this.settings.timeout): AtomicPromise<R> | void {
    if (typeof callback !== 'function') return new AtomicPromise<R>((resolve, reject) =>
      void this.call(name, param, (err, result) => err ? reject(err) : resolve(result), callback));
    this.throwErrorIfNotAvailable();
    this.messages.push(
      [typeof name === 'string' ? [name] : new NamePool(this.workers, name),
      param,
      callback,
      Date.now() + timeout,
      0,
    ]);
    while (this.messages.length > (this.available ? this.settings.capacity : 0)) {
      const { 0: names, 1: param, 2: callback, 4: timer } = this.messages.shift()!;
      timer && clearTimeout(timer);
      const name: N | undefined = names[Symbol.iterator]().next().value;
      this.$events?.loss.emit([name], [name, param]);
      try {
        callback(new Error(`Spica: Supervisor: <${this.name}>: Message overflowed`), undefined);
      }
      catch (reason) {
        causeAsyncException(reason);
      }
    }
    if (this.messages.length === 0) return;
    this.throwErrorIfNotAvailable();
    this.schedule();
    if (timeout > 0 && timeout !== Infinity) {
      assert(this.messages.at(-1)![4] === 0);
      this.messages.at(-1)![4] = setTimeout(() =>
        void this.schedule()
      , timeout + 3);
    }
  }
  public cast(name: N | ((names: Iterable<N>) => Iterable<N>), param: P, timeout = this.settings.timeout): AtomicPromise<R> | undefined {
    this.throwErrorIfNotAvailable();
    const expire = Date.now() + timeout;
    let result: AtomicPromise<R> | undefined;
    for (name of typeof name === 'string' ? [name] : new NamePool(this.workers, name)) {
      if (result = this.workers.get(name)?.call([param, expire])) break;
    }
    if (result) return result;
    const n = typeof name === 'string' ? name : undefined;
    this.$events?.loss.emit([n], [n, param]);
  }
  public refs(name?: N): [N, Supervisor.Process.Regular<P, R, S>, S, (reason?: unknown) => boolean][] {
    assert(this.available || this.workers.size === 0);
    return name === undefined
      ? [...this.workers.values()].map(convert)
      : this.workers.has(name)
        ? [convert(this.workers.get(name)!)]
        : [];

    function convert(worker: Worker<N, P, R, S>): [N, Supervisor.Process.Regular<P, R, S>, S, (reason?: unknown) => boolean] {
      assert(worker instanceof Worker);
      return [
        worker.name,
        worker.process,
        worker.state,
        worker.terminate,
      ];
    }
  }
  public kill(name: N, reason?: unknown): boolean {
    if (!this.available) return false;
    assert(this.alive === true);
    return this.workers.has(name)
      ? this.workers.get(name)!.terminate(reason)
      : false;
  }
  public clear(reason?: unknown): void {
    while (this.workers.size > 0) {
      for (const worker of this.workers.values()) {
        worker.terminate(reason);
      }
    }
  }
  public terminate(reason?: unknown): boolean {
    if (!this.available) return false;
    assert(this.alive === true);
    this.destructor(reason);
    this[Coroutine.exit](undefined);
    return true;
  }
  public override [Coroutine.terminate](reason?: unknown): void {
    this.terminate(reason);
  }
  public override [Coroutine.port] = {
    ask: () => { throw new Error(`Spica: Supervisor: <${this.name}>: Cannot use coroutine port`); },
    recv: () => { throw new Error(`Spica: Supervisor: <${this.name}>: Cannot use coroutine port`); },
    send: () => { throw new Error(`Spica: Supervisor: <${this.name}>: Cannot use coroutine port`); },
    connect: () => { throw new Error(`Spica: Supervisor: <${this.name}>: Cannot use coroutine port`); },
  } as const;
  private scheduled = false;
  private schedule(): void {
    if (!this.available || this.scheduled || this.messages.length === 0) return;
    this.scheduled = true;
    const p = new AtomicFuture(false);
    p.finally(() => {
      this.scheduled = false;
      this.deliver();
    });
    this.settings.scheduler.call(undefined, p.bind);
    this.settings.scheduler === requestAnimationFrame && setTimeout(p.bind, 1000);
  }
  // Bug: Karma and TypeScript
  private readonly messages = new Ring<[Iterable<N>, P, Supervisor.Callback<R>, number, ReturnType<typeof setTimeout> | 0]>();
  private deliver(): void {
    if (!this.available) return;
    assert(!this.scheduled);
    const since = Date.now();
    for (let len = this.messages.length, i = 0; this.available && i < len; ++i) {
      if (this.settings.resource - (Date.now() - since) <= 0) return void this.schedule();
      const { 0: names, 1: param, 2: callback, 3: expiration, 4: timer } = this.messages.at(i)!;
      let result: AtomicPromise<R> | undefined;
      let name: N | undefined;
      for (name of typeof names === 'string' ? [names] : names) {
        if (Date.now() > expiration) break;
        if (result = this.workers.get(name)?.call([param, expiration])) break;
      }
      if (!result && Date.now() < expiration) continue;
      this.messages.splice(i, 1);
      --i;
      --len;
      timer && clearTimeout(timer);

      if (result) {
        result.then(
          reply =>
            void callback(undefined, reply),
          () =>
            void callback(new Error(`Spica: Supervisor: <${this.name}>: Process failed`), undefined));
      }
      else {
        this.$events?.loss.emit([name], [name, param]);
        try {
          callback(new Error(`Spica: Supervisor: <${this.name}>: Message expired`), undefined);
        }
        catch (reason) {
          causeAsyncException(reason);
        }
      }
    }
  }
}
export namespace Supervisor {
  export type Process<P, R = P, S = undefined> =
    | Process.Regular<P, R, S>
    | Process.Function<P, R, S>
    | Process.GeneratorFunction<P, R, S>
    | Process.AsyncGeneratorFunction<P, R, S>
    | Process.Coroutine<P, R>;
  export namespace Process {
    export type Regular<P, R, S> = {
      readonly init: (state: S, kill: (reason?: unknown) => void) => S;
      readonly main: Function<P, R, S>;
      readonly exit: (reason: unknown, state: S) => void;
    };
    export type Function<P, R, S> = (param: P, state: S, kill: (reason?: unknown) => void) => Result<R, S> | PromiseLike<Result<R, S>>;
    export type GeneratorFunction<P, R, S> = (state: S, kill: (reason?: unknown) => void) => Generator<R, R, P>;
    export type AsyncGeneratorFunction<P, R, S> = (state: S, kill: (reason?: unknown) => void) => AsyncGenerator<R, R, P>;
    export type Coroutine<P, R> = ICoroutine<R, R, P>;
    export type Result<R, S> = readonly [R, S];
  }
  export type Callback<R> = (...args: [error: undefined, reply: R] | [error: Error, reply: undefined]) => void;
  export namespace Event {
    export namespace Data {
      export type Init<N extends string, P, R, S> = readonly [N, Process.Regular<P, R, S>, S];
      export type Exit<N extends string, P, R, S> = readonly [N, Process.Regular<P, R, S>, S, unknown];
      export type Loss<N extends string, P> = readonly [N | undefined, P];
    }
  }
}

function isAsyncGeneratorFunction<P, R, S>(process: Supervisor.Process<P, R, S>): process is Supervisor.Process.AsyncGeneratorFunction<P, R, S> {
  return process[Symbol.toStringTag] === 'AsyncGeneratorFunction';
}

function isGeneratorFunction<P, R, S>(process: Supervisor.Process<P, R, S>): process is Supervisor.Process.GeneratorFunction<P, R, S> {
  return process[Symbol.toStringTag] === 'GeneratorFunction';
}

class NamePool<N extends string> implements Iterable<N> {
  constructor(
    private readonly workers: ReadonlyMap<N, unknown>,
    private readonly selector: (names: Iterable<N>) => Iterable<N>,
  ) {
  }
  public [Symbol.iterator](): Iterator<N, undefined, undefined> {
    return this.selector(this.workers.keys())[Symbol.iterator]();
  }
}

class Worker<N extends string, P, R, S> {
  constructor(
    public readonly name: N,
    public readonly process: Supervisor.Process.Regular<P, R, S>,
    public state: S,
    private readonly sv: Supervisor<N, P, R, S>,
    private readonly schedule: () => void,
    initiated: boolean,
    private readonly events: {
      readonly init: Publisher<[N], Supervisor.Event.Data.Init<N, P, R, S>, unknown>;
      readonly exit: Publisher<[N], Supervisor.Event.Data.Exit<N, P, R, S>, unknown>;
    } | undefined,
    private readonly destructor_: () => void,
  ) {
    initiated && this.init();
  }
  private destructor(reason: unknown): void {
    assert(this.alive === true);
    this.alive = false;
    this.available = false;
    Object.freeze(this);
    assert(this.alive === false);
    assert(this.available === false);
    try {
      this.destructor_();
    }
    catch (reason) {
      causeAsyncException(reason);
    }
    if (this.initiated) {
      this.exit(reason);
    }
  }
  private alive = true;
  private available = true;
  private initiated = false;
  private init(): void {
    assert(!this.initiated);
    this.initiated = true;
    this.events?.init
      .emit([this.name], [this.name, this.process, this.state]);
    this.state = this.process.init(this.state, this.terminate);
  }
  private exit(reason: unknown): void {
    assert(this.initiated);
    try {
      this.process.exit(reason, this.state);
      this.events?.exit
        .emit([this.name], [this.name, this.process, this.state, reason]);
    }
    catch (reason_) {
      this.events?.exit
        .emit([this.name], [this.name, this.process, this.state, reason]);
      this.sv.terminate(reason_);
    }
  }
  public call([param, expiration]: [P, number]): AtomicPromise<R> | undefined {
    if (!this.available) return;
    return new AtomicPromise<Supervisor.Process.Result<R, S>>((resolve, reject) => {
      isFinite(expiration) && setTimeout(() => void reject(new Error()), expiration - Date.now());
      assert(this.alive);
      assert(this.available);
      this.available = false;
      if (!this.initiated) {
        this.init();
        if (!this.alive) return void reject();
      }
      assert(this.alive);
      assert(!this.available);
      AtomicPromise.resolve(this.process.main(param, this.state, this.terminate))
        .then(resolve, reject);
    })
      .then(([reply, state]) => {
        if (this.alive) {
          this.schedule();
          assert(!Object.isFrozen(this));
          this.state = state;
          this.available = true;
        }
        return reply;
      })
      .catch(reason => {
        this.schedule();
        this.terminate(reason);
        throw reason;
      });
  }
  public readonly terminate = (reason: unknown): boolean => {
    if (!this.alive) return false;
    this.destructor(reason);
    return true;
  };
}
