/**
 * The actions API is meant for registering quick actions that may be
 * useful for other plugins, or just have the ability to call on the fly.
 * @module Actions
 * @memberof API
 * @namespace API.Actions
 * @version 1.0.0
 */

import { assertString } from '@vizality/util/string';
import { getCaller } from '@vizality/util/file';
import { Events } from '@vizality/constants';
import { API } from '@vizality/entities';

/**
 * @extends API
 * @extends Events
 */
export default class Actions extends API {
  constructor () {
    super();
    /**
     * All currently registered actions.
     */
    this.actions = [];
  }

  /**
   * Shuts down the API, removing all listeners and stored objects.
   */
  stop () {
    this.unregisterAllActions();
    this.removeAllListeners();
    delete vizality.api.actions;
  }

  /**
   * Registers an action.
   * @param {string} actionId Action ID
   * @param {Function} executor Action executor
   * @emits Actions#Events.VIZALITY_ACTION_ADD
   */
  registerAction (actionId, executor) {
    try {
      assertString(actionId);
      if (this.isAction(actionId)) {
        throw new Error(`Action "${actionId}" is already registered!`);
      }
      if (!executor) {
        throw new Error('Action must contain an executor!');
      }
      if (typeof executor !== 'function') {
        throw new TypeError('Action executor must be a function!');
      }
      const caller = getCaller();
      this.actions.push({
        id: actionId,
        executor,
        caller
      });
      this.emit(Events.VIZALITY_ACTION_ADD, actionId);
    } catch (err) {
      return this.error(this._labels.concat('registerAction'), err);
    }
  }

  /**
   * Invokes an action executor.
   * @param {string} actionId Action ID
   * @returns {Promise<void>}
   */
  async invokeAction (actionId) {
    try {
      assertString(actionId);
      if (!this.isAction(actionId)) {
        throw new Error(`Action "${actionId}" could not be found!`);
      }
      try {
        await this.getActionById(actionId).executor();
      } catch (err) {
        return this.error(this._labels.concat('invokeAction'), `There was a problem invoking action "${actionId}" executor!`, err);
      }
    } catch (err) {
      return this.error(this._labels.concat('invokeAction'), err);
    }
  }

  /**
   * Checks if an action is registered.
   * @param {string} actionId Action ID
   */
  isAction (actionId) {
    try {
      assertString(actionId);
      return Boolean(this.getActionById(actionId));
    } catch (err) {
      return this.error(this._labels.concat('isAction'), err);
    }
  }

  /**
   * Gets the first action found matching a given filter.
   * @param {Function} filter Function to use to filter actions by
   * @returns {object|null} Action matching a given filter
   */
  getAction (filter) {
    try {
      if (!filter?.length) return null;
      return this.actions.find(filter);
    } catch (err) {
      return this.error(this._labels.concat('getAction'), err);
    }
  }

  /**
   * Gets an action matching a given ID.
   * @param {string} actionId Action ID
   * @returns {object|null} Action matching a given ID
   */
  getActionById (actionId) {
    try {
      assertString(actionId);
      return this.actions.find(action => action.id === actionId);
    } catch (err) {
      return this.error(this._labels.concat('getActionById'), err);
    }
  }

  /**
   * Gets all actions found matching a given filter.
   * @param {Function} filter Function to use to filter actions by
   * @returns {Array<object|null>} Actions matching a given filter
   */
  getActions (filter) {
    try {
      if (!filter?.length) return null;
      return this.actions.filter(filter);
    } catch (err) {
      return this.error(this._labels.concat('getActions'), err);
    }
  }

  /**
   * Gets all actions matching a given caller.
   * @param {string} addonId Addon ID
   * @returns {Array<object|null>} Actions matching a given caller
   */
  getActionsByCaller (addonId) {
    try {
      assertString(addonId);
      return this.actions.filter(action => action.caller?.id === addonId);
    } catch (err) {
      return this.error(this._labels.concat('getActionsByCaller'), err);
    }
  }

  /**
   * Gets all actions.
   * @returns {Array<object|null>} All actions
   */
  getAllActions () {
    try {
      return this.actions;
    } catch (err) {
      return this.error(this._labels.concat('getAllActions'), err);
    }
  }

  /**
   * Unregisters an action.
   * @param {string} actionId Action ID
   * @emits Actions#Events.VIZALITY_ACTION_REMOVE
   */
  unregisterAction (actionId) {
    try {
      assertString(actionId);
      if (this.isAction(actionId)) {
        this.actions = this.getActions(action => action.id !== actionId);
        this.emit(Events.VIZALITY_ACTION_REMOVE, actionId);
      } else {
        throw new Error(`Action "${actionId}" is not registered, so it cannot be unregistered!`);
      }
    } catch (err) {
      return this.error(this._labels.concat('unregisterAction'), err);
    }
  }

  /**
   * Unregisters all actions matching a given caller.
   * @param {string} addonId Addon ID
   * @emits Actions#Events.VIZALITY_ACTION_REMOVE_ALL_BY_CALLER
   */
  unregisterActionsByCaller (addonId) {
    try {
      assertString(addonId);
      this.actions = this.actions.filter(action => action.caller?.id !== addonId);
      this.emit(Events.VIZALITY_ACTION_REMOVE_ALL_BY_CALLER, addonId);
    } catch (err) {
      return this.error(this._labels.concat('unregisterActionsByCaller'), err);
    }
  }

  /**
   * Unregisters all actions.
   * @emits Actions#Events.VIZALITY_ACTION_REMOVE_ALL
   */
  unregisterAllActions () {
    try {
      this.actions = [];
      this.emit(Events.VIZALITY_ACTION_REMOVE_ALL);
    } catch (err) {
      return this.error(this._labels.concat('unregisterAllActions'), err);
    }
  }
}
