// Copyright 2017-2025 @pezkuwi/api authors & contributors
// SPDX-License-Identifier: Apache-2.0

import type { Callback } from '@pezkuwi/types/types';
import type { UnsubscribePromise } from '../types/index.js';

import { isFunction, noop } from '@pezkuwi/util';

export type CombinatorCallback <T extends unknown[]> = Callback<T>;

export type CombinatorFunction = (cb: Callback<any>) => UnsubscribePromise;

export class Combinator<T extends unknown[] = unknown[]> {
  #allHasFired = false;
  #callback: CombinatorCallback<T>;
  #fired: boolean[] = [];
  #fns: CombinatorFunction[] = [];
  #isActive = true;
  #results: unknown[] = [];
  #subscriptions: UnsubscribePromise[] = [];

  constructor (fns: (CombinatorFunction | [CombinatorFunction, ...unknown[]])[], callback: CombinatorCallback<T>) {
    this.#callback = callback;

    // eslint-disable-next-line @typescript-eslint/no-floating-promises, @typescript-eslint/require-await
    this.#subscriptions = fns.map(async (input, index): UnsubscribePromise => {
      const [fn, ...args] = Array.isArray(input)
        ? input
        : [input];

      this.#fired.push(false);
      this.#fns.push(fn);

      // Not quite 100% how to have a variable number at the front here
      // eslint-disable-next-line @typescript-eslint/no-unsafe-return,@typescript-eslint/ban-types
      return (fn as Function)(...args, this._createCallback(index));
    });
  }

  protected _allHasFired (): boolean {
    this.#allHasFired ||= this.#fired.filter((hasFired): boolean => !hasFired).length === 0;

    return this.#allHasFired;
  }

  protected _createCallback (index: number): (value: any) => void {
    return (value: unknown): void => {
      this.#fired[index] = true;
      this.#results[index] = value;

      this._triggerUpdate();
    };
  }

  protected _triggerUpdate (): void {
    if (!this.#isActive || !isFunction(this.#callback) || !this._allHasFired()) {
      return;
    }

    try {
      Promise
        .resolve(this.#callback(this.#results as T))
        .catch(noop);
    } catch {
      // swallow, we don't want the handler to trip us up
    }
  }

  public unsubscribe (): void {
    if (!this.#isActive) {
      return;
    }

    this.#isActive = false;

    Promise
      .all(this.#subscriptions.map(async (subscription): Promise<void> => {
        try {
          const unsubscribe = await subscription;

          if (isFunction(unsubscribe)) {
            unsubscribe();
          }
        } catch {
          // ignore
        }
      })).catch(() => {
        // ignore, already ignored above, should never throw
      });
  }
}
