/* global haxroomie */
/**
 * Object containing HHM plugin name and content.
 *
 * @typedef {Object} Plugin
 * @property {string} [name] - Plugins name. Can be overriden by the plugin
 *    itself if it defines the `pluginSpec.name` property.
 * @property {string} content - UTF-8 encoded content of the plugin.
 */

/**
 * Object containing information about a plugin.
 *
 * @typedef {Object} PluginData
 * @property {number} id - The plugin id.
 * @property {string|number} name - The plugin name.
 * @property {boolean} isEnabled - Indicates whether the plugin is enabled or disabled.
 * @property {object} pluginSpec - HHM pluginSpec property.
 * @property {object} pluginSpecOriginal - Plugins initial pluginSpec (default values).
 */

/**
 * Class for controlling Haxball Headless Manager (HHM) plugins.
 */
class PluginController {
  constructor(opt) {
    this.page = opt.page;
  }

  /**
   * Returns loaded plugins.
   *
   * @returns {Promise<Array.<PluginData>>} - Array of plugins.
   */
  async getPlugins() {
    let result = await this.page.evaluate(() => {
      let plugins = HHM.manager
        .getLoadedPluginIds()
        .map((id) => haxroomie.serializePlugin(id))
        .filter((pluginData) => {
          const name = pluginData.pluginSpec.name;
          // ignore these plugins
          if (!haxroomie.ignoredPlugins) return true;
          return !haxroomie.ignoredPlugins.has(name);
        });
      return plugins;
    });
    return result;
  }

  /**
   * Returns PluginData of the given plugin name.
   *
   * @param {string} name - Name of the plugin.
   * @returns {Promise.<?PluginData>} - Data of the plugin or `null` if
   *    plugin was not found.
   */
  async getPlugin(name) {
    return this.page.evaluate((name) => {
      const plugin = HHM.manager.getPlugin(name);
      return haxroomie.serializePlugin(plugin);
    }, name);
  }

  /**
   * Enables a HHM plugin with the given name.
   *
   * @param {string} name - Name of the plugin
   * @returns {Promise.<boolean>} - `true` if plugin was enabled, `false` otherwise.
   */
  async enablePlugin(name) {
    return this.page.evaluate((name) => {
      return HHM.manager.enablePlugin(name);
    }, name);
  }

  /**
   * Disables a HHM plugin with the given name.
   *
   * If the name is an Array then
   * it disables all the plugins in the given order.
   *
   * @param {string} name - Name the plugin.
   * @param {boolean} [recursive=false] - If true all the plugins that depend on
   *    the plugin will get disabled also.
   * @returns {Promise.<Array.<number>>} - Array of disabled plugin IDs or
   *    empty array if the plugin could not be disabled or was already disabled.
   */
  async disablePlugin(name, recursive = false) {
    return this.page.evaluate(
      async (name, recursive) => {
        return HHM.manager.disablePlugin(name, recursive);
      },
      name,
      recursive
    );
  }

  /**
   * Gets a list of plugins that depend on the given plugin.
   *
   * @param {string} name - Name or id of the plugin.
   * @param {boolean} [recursive=true] - Finds indirect dependencies also.
   * @param {boolean} [includeDisabled=false] - Include disabled plugins
   * @returns {Promise<Array.<PluginData>>} - Array of plugins.
   */
  async getPluginsThatDependOn(
    name,
    recursive = true,
    includeDisabled = false
  ) {
    return this.page.evaluate(
      (name, recursive, includeDisabled) => {
        return HHM.manager
          .getDependentPlugins(name, recursive, includeDisabled)
          .map((id) => haxroomie.serializePlugin(id));
      },
      name,
      recursive,
      includeDisabled
    );
  }

  /**
   * Checks if the room has a plugin with given name loaded.
   * @param {string} name - Name of the plugin.
   * @returns {Promise.<boolean>} - `true` if it had the plugin, `false` if not.
   */
  async hasPlugin(name) {
    return this.page.evaluate(async (name) => {
      return HHM.manager.hasPlugin(name);
    }, name);
  }

  /**
   * Adds a new plugin.
   *
   * If the `plugin` is `string`, then it will be loaded from the available
   * repositories.
   *
   * If the `plugin` is [Plugin]{@link Plugin}, then it will be loaded
   * from contents of `Plugin`.
   *
   * @param {string|Plugin} plugin - Plugins name if loading from
   *    repositories or plugin definition if loading it from an object.
   * @param {object} [pluginConfig] - Configuration options for the plugin.
   * @returns {Promise.<number>} - Plugin ID if the plugin and all of its dependencies
   *    have been loaded, -1 otherwise.
   */
  async addPlugin(plugin, pluginConfig) {
    if (!plugin) {
      throw new TypeError('Missing required argument: plugin');
    }

    if (typeof plugin === 'string') {
      return this.page.evaluate(
        async (name, pluginConfig) => {
          let id = await HHM.manager.addPlugin({ pluginName: name });
          if (id >= 0 && pluginConfig) {
            HHM.manager.setPluginConfig(id, pluginConfig);
          }
          return id;
        },
        plugin,
        pluginConfig
      );
    }

    if (!plugin.content) {
      throw new TypeError('Plugin is missing required property: content');
    }

    return this.page.evaluate(
      async (plugin, pluginConfig) => {
        let id = await HHM.manager.addPlugin({
          pluginCode: plugin.content,
          pluginName: plugin.name,
        });
        if (id >= 0 && pluginConfig) {
          HHM.manager.setPluginConfig(id, pluginConfig);
        }
        return id;
      },
      plugin,
      pluginConfig
    );
  }

  /**
   * Removes a plugin.
   *
   * @param {string} pluginName - Plugins name.
   * @param {boolean} [safe=true] - Remove plugin safely (see HHM
   *    PluginManager#removePlugin).
   * @returns {Promise.<boolean>} - Whether the removal was successful.
   */
  async removePlugin(pluginName, safe = true) {
    if (!pluginName) {
      throw new TypeError('Missing required argument: pluginName');
    }

    return this.page.evaluate(
      async (pluginName, safe) => {
        let id = HHM.manager.getPluginId(pluginName);
        return HHM.manager.removePlugin(id, safe);
      },
      pluginName,
      safe
    );
  }

  /**
   * Sets the rooms plugin config. Merges the new config with the old one.
   *
   * Tries to load plugins that are not loaded from the available
   * repositories and removes the loaded plugins that are not in the given
   * config.
   *
   * If `pluginName` is given then only config for the given plugin
   * is set.
   * @param {object} pluginConfig - Room wide config or plugins config.
   * @param {string} [pluginName] - Name of the plugin if wanting to change
   *    config of only one plugin.
   */
  async setPluginConfig(pluginConfig, pluginName) {
    if (!pluginConfig) {
      throw new TypeError('Missing required argument: pluginConfig');
    }
    if (typeof pluginConfig !== 'object') {
      throw new TypeError('typeof pluginConfig should be object');
    }

    // Set the config for one plugin if plugins name is given.
    if (typeof pluginName === 'string') {
      await this.page.evaluate(
        async (pluginName, pluginConfig) => {
          let pluginId = HHM.manager.getPluginId(pluginName);

          if (pluginId < 0) {
            pluginId = await HHM.manager.addPlugin({ pluginName });
            if (pluginId < 0) {
              throw new Error(
                `Cannot load plugin "${pluginName}" from available repositories.`
              );
            }
          }

          let plugin = HHM.manager.getPlugin(pluginName);

          // get plugins default config
          const pluginDefaultConfig = plugin._pluginSpecOriginal.config
            ? plugin._pluginSpecOriginal.config
            : {};

          // merge the new config with plugins default values
          HHM.manager.setPluginConfig(pluginId, {
            ...pluginDefaultConfig,
            ...pluginConfig,
          });
        },
        pluginName,
        pluginConfig
      );
      return;
    }

    // Change the configs for all the plugins if no plugin name is given.
    for (let [name, config] of Object.entries(pluginConfig)) {
      await this.page.evaluate(
        async (name, config) => {
          let pluginId = HHM.manager.getPluginId(name);

          if (pluginId < 0) {
            pluginId = await HHM.manager.addPlugin({ pluginName: name });
            if (pluginId < 0) {
              return;
            }
          }

          let plugin = HHM.manager.getPlugin(name);

          // get plugins default config
          const pluginDefaultConfig = plugin._pluginSpecOriginal.config
            ? plugin._pluginSpecOriginal.config
            : {};

          // merge the new config with plugins default values
          HHM.manager.setPluginConfig(pluginId, {
            ...pluginDefaultConfig,
            ...config,
          });
        },
        name,
        config
      );
    }

    await this.prunePlugins({ pluginConfig });
  }

  /**
   * Removes the plugins that are not in the given `pluginConfig`.
   *
   * Goes over the loaded plugins that are not in the pluginConfig and
   * checks if they are a direct or indirect dependency of one of the plugins
   * listed in the pluginConfig and remove them if not.
   * @private
   */
  async prunePlugins({ pluginConfig }) {
    const loadedPlugins = await this.getPlugins();

    for (let plugin of loadedPlugins) {
      if (Object.prototype.hasOwnProperty.call(pluginConfig, plugin.name)) {
        continue;
      }

      let dependentPlugins = await this.getPluginsThatDependOn(
        plugin.name,
        true,
        true
      );

      let shouldBeRemoved = true;
      for (let dependent of dependentPlugins) {
        if (!dependent) continue;
        if (
          Object.prototype.hasOwnProperty.call(pluginConfig, dependent.name)
        ) {
          shouldBeRemoved = false;
          break;
        }
      }
      if (shouldBeRemoved) {
        await this.disablePlugin(plugin.name, true);
        await this.removePlugin(plugin.name, false);
      }
    }
  }

  /**
   * Returns the plugin config for all loaded plugins in the room or
   * if `pluginName` is given, then return the config for that plugin.
   *
   * @param {string} [pluginName] - The name of the plugin.
   * @returns {Promise.<object>} - The config object of plugin(s).
   */
  async getPluginConfig(pluginName) {
    if (typeof pluginName === 'string') {
      let config = await this.page.evaluate((pluginName) => {
        let plugin = HHM.manager.getPlugin(pluginName);
        if (!plugin) {
          throw new TypeError(`Invalid plugin "${pluginName}".`);
        }

        return plugin.getConfig();
      }, pluginName);
      return config;
    }

    let config = await this.page.evaluate(() => {
      let plugins = HHM.manager.getLoadedPluginIds().map((id) => {
        return HHM.manager.getPlugin(id);
      });
      let cfg = {};
      for (let plugin of plugins) {
        cfg[plugin] = plugin.getConfig();
      }
      return cfg;
    });
    return config;
  }

  /**
   * Reloads a plugin from the configured repositories.
   *
   * This is a wrapper around the HHM PluginManager.reloadPlugin method.
   * See https://github.com/saviola777/haxball-headless-manager/blob/master/src/classes/PluginManager.js#L565
   *
   * @param {string} pluginName Plugin name to be reloaded.
   * @param {boolean} [safe] Whether to disable dependent plugins before
   *  unloading the given plugin.
   * @returns {boolean} Whether the plugin was successfully reloaded
   * @throws {Error} If the given plugin is not loaded or if safe mode was
   *  enabled but the plugin can't be disabled.
   */
  async reloadPlugin(pluginName, safe = true) {
    return this.page.evaluate(
      (pluginName, safe) => {
        return HHM.manager.reloadPlugin(pluginName, safe);
      },
      pluginName,
      safe
    );
  }
}

module.exports = PluginController;