import { MelEnumerable } from './classes/enum.js';

export {
  RotomecaEvent as BnumEvent,
  MelConditionnalEventItem,
  MelConditionnalEvent,
};

/**
 * @class
 * @classdesc Représente un évènement. On lui ajoute ou supprime des callbacks, et on les appelle les un après les autres.
 * @alias BnumEvent
 * @template T
 */
class RotomecaEvent {
  constructor() {
    /**
     * Liste des évènements à appeler
     * @type {Object<string, T>}
     * @member
     */
    this.events = {};
    /**
     *	Compteur d'évènements
     * @type {number}
     * @private
     */
    this._count = 0;
  }

  /**
   * Ajoute un callback
   * @param {T} event Callback qui sera appelé lors de l'appel de l'évènement
   * @param  {...any} args Liste des arguments qui seront passé aux callback
   * @returns {string} Clé créée
   */
  push(event, ...args) {
    const key = this._generateKey();
    this.events[key] = { args, callback: event };
    ++this._count;
    return key;
  }

  /**
   * Ajoute un callback avec un clé qui permet de le retrouver plus tard
   * @param {string} key Clé de l'évènement
   * @param {T} event Callback qui sera appelé lors de l'appel de l'évènement
   * @param  {...any} args Liste des arguments qui seront passé aux callback
   */
  add(key, event, ...args) {
    if (!this.events[key]) ++this._count;

    this.events[key] = { args, callback: event };
  }

  /**
   * Vérifie si une clé éxiste
   * @param {string} key
   * @returns {boolean}
   */
  has(key) {
    return !!this.events[key];
  }

  /**
   * Supprime un callback
   * @param {string} key Clé
   */
  remove(key) {
    this.events[key] = null;

    --this._count;
  }

  /**
   * Met les count à jours si il y a des modification directement via `events`
   * @returns {RotomecaEvent<T>}
   */
  rebase() {
    let rebased = MelEnumerable.from(this.events).where((x) => !!x?.value);

    this.events = rebased.toJsonDictionnary(
      (x) => x.key,
      (x) => x.value,
    );
    this._count = rebased.count();

    rebased = null;
    return this;
  }

  /**
   * Renvoie si il y a des évènements ou non.
   * @returns {boolean}
   */
  haveEvents() {
    return this.count() > 0;
  }

  /**
   * Affiche le nombre d'évènements
   * @returns {number}
   */
  count() {
    return this._count;
  }

  /**
   * Génère une clé pour l'évènement
   * @private
   * @returns {string}
   */
  _generateKey() {
    const g_key =
      window?.mel_metapage?.Functions?.generateWebconfRoomName?.() ||
      Math.random() * (this._count + 10);

    let ae = false;
    for (const key in this.events) {
      if (Object.hasOwnProperty.call(this.events, key)) {
        if (key === g_key) {
          ae = true;
          break;
        }
      }
    }

    if (ae) return this._generateKey();
    else return g_key;
  }

  /**
   * Appèle les callbacks
   * @param  {...any} params Paramètres à envoyer aux callbacks
   * @returns {null | any | Array}
   */
  call(...params) {
    let results = {};
    const keys = Object.keys(this.events);

    if (keys.length !== 0) {
      for (let index = 0, len = keys.length; index < len; ++index) {
        const key = keys[index];

        if (this.events[key]) {
          const { args, callback } = this.events[key];

          if (callback)
            results[key] = this._call_callback(
              callback,
              ...[...args, ...params],
            );
        }
      }
    }

    switch (Object.keys(results).length) {
      case 0:
        return null;
      case 1:
        return results[Object.keys(results)[0]];
      default:
        return results;
    }
  }

  /**
   * Lance un callback
   * @param {T} callback Callback à appeler
   * @param  {...any} args Paramètres à envoyer aux callbacks
   * @returns {*}
   */
  _call_callback(callback, ...args) {
    return callback(...args);
  }

  /**
   * Appèle les callbacks
   * @param  {...any} params Paramètres à envoyer aux callbacks
   * @returns {Promise<null | any | Array>}
   * @async
   */
  async asyncCall(...params) {
    let asyncs = [];
    for (const key in this.events) {
      if (Object.hasOwnProperty.call(this.events, key)) {
        const { args, callback } = this.events[key];
        if (callback)
          asyncs.push(this._call_callback(callback, ...[...args, ...params]));
      }
    }

    const results = (await Promise.allSettled(asyncs)).map((x) => x.value);

    switch (results.length) {
      case 0:
        return null;
      case 1:
        return results[Object.keys(results)[0]];
      default:
        return results;
    }
  }

  /**
   * Vide la classe
   */
  clear() {
    this.events = {};
    this._count = 0;
  }
}

class MelConditionnalEventItem {
  constructor({ action = (...args) => args, aditionnalDatas = null }) {
    this._init()._setup(action, aditionnalDatas);
  }

  _init() {
    this.action = (...args) => args;
    this.datas = null;
    return this;
  }

  _setup(...args) {
    const { action, datas } = args;
    this.action = action;
    this.datas = datas;
    return this;
  }
}

class MelConditionnalEvent extends RotomecaEvent {
  constructor() {
    super();
  }

  *yieldCall(validCondition, ...params) {
    const keys = Object.keys(this.events);

    if (keys.length !== 0) {
      for (let index = 0, len = keys.length; index < len; ++index) {
        const key = keys[index];
        const { args, callback } = this.events[key];

        if (!!callback && validCondition(key, callback.datas))
          yield callback.action(...[...args, ...params]);
      }
    }
  }

  call(validCondition, ...args) {
    [...this.yieldCall(validCondition, ...args)];
  }

  pushConditionnalItem({
    action = (...args) => args,
    additionnalDatas = null,
  }) {
    this.push(new MelConditionnalEventItem(action, additionnalDatas));
    return this;
  }
}