import { log, warn, error, PRESET_LABELS } from '@vizality/util/logger';
import { toPlural, toTitleCase } from '@vizality/util/string';
import { resolveCompiler } from '@vizality/compilers';
import { unpatchAll } from '@vizality/patcher';
import { jsonToReact } from '@vizality/util/react';
import { createElement } from '@vizality/util/dom';
import { Directories } from '@vizality/constants';
import { isArray } from '@vizality/util/array';
import { debounce } from 'lodash';
import { join, sep, extname } from 'path';
import { watch } from 'chokidar';
import { existsSync, readFileSync } from 'fs';

import Updatable from './Updatable';

/*
 * @property {boolean} _ready Whether the plugin is ready or not
 * @property {SettingsCategory} settings Plugin settings
 * @property {object<string, Compiler>} styles Styles the plugin loaded
 */

/**
 * @extends Updatable
 * @extends Events
 */
export default class Plugin extends Updatable {
  constructor () {
    super(Directories.PLUGINS);
    this.settings = vizality.api.settings._buildCategoryObject(this.addonId);
    this.styles = {};
    this.sections = {};
    this.type = 'plugin';
    this._ready = false;
    this._watcherEnabled = null;
    this._watcher = {};
    this._labels = [ 'Plugin', this.manifest?.name || this.constructor?.name ];
    this._registerSections();
  }

  /**
   * Injects a style element containing the styles from the specified stylesheet into the
   * document head. Style element (and styles) are automatically removed on
   * plugin disable/unload.
   * @param {string} path Stylesheet path. Either absolute or relative to the plugin root
   * @param {boolean} [suppress=false] Whether or not to suppress errors in console
   */
  injectStyles (path, suppress = false) {
    let compiler, style, compiled, id;
    try {
      let resolvedPath = path;
      if (!existsSync(resolvedPath)) {
        // Assume it's a relative path and try resolving it
        resolvedPath = join(this.path, path);
        if (!existsSync(resolvedPath)) {
          throw new Error(`Cannot find "${path}"! Make sure the file exists and try again.`);
        }
      }

      id = Math.random().toString(36).slice(2);
      compiler = resolveCompiler(resolvedPath);
      style = createElement('style', {
        id: `${this.type}-${this.addonId}-${id}`,
        'vz-style': '',
        [`vz-${this.type}`]: ''
      });

      document.head.appendChild(style);
    } catch (err) {
      return this.error(err);
    }

    const compile = debounce(async () => {
      try {
        compiled = await compiler.compile();
        style.innerHTML = compiled;
      } catch (err) {
        if (!suppress) {
          return this.error('There was a problem compiling!', err);
        }
      }
    }, 300);

    try {
      this.styles[id] = {
        compiler,
        compile
      };

      compiler.enableWatcher();
      compiler.on('src-update', compile);
      return compile();
    } catch (err) {
      return this.error(err);
    }
  }

  /**
   *
   * @param {*} render
   */
  registerSettings (render) {
    vizality.api.settings.registerSettings({
      type: this.type,
      addonId: this.addonId,
      render
    });
  }

  /**
   *
   * @param  {...any} message
   */
  log (...message) {
    // In case the addon wants to provide their own labels
    if (isArray(message[0])) {
      const _message = message.slice(1);
      log({ labels: message[0], message: _message });
    } else {
      log({ labels: this._labels, message });
    }
  }

  /**
   *
   * @param  {...any} message
   */
  warn (...message) {
    // In case the addon wants to provide their own labels
    if (isArray(message[0])) {
      const _message = message.slice(1);
      warn({ labels: message[0], message: _message });
    } else {
      warn({ labels: this._labels, message });
    }
  }

  /**
   *
   * @param  {...any} message
   */
  error (...message) {
    // In case the addon wants to provide their own labels
    if (isArray(message[0])) {
      const _message = message.slice(1);
      error({ labels: message[0], message: _message });
    } else {
      error({ labels: this._labels, message });
    }
  }

  /**
   * Update the addon.
   * @private
   * @param {boolean} [force=false] Whether to force update the addon
   * @returns {Promise<boolean>}
   */
  async _update (force = false) {
    try {
      const success = await super._update(force);
      if (success && this._ready) {
        this.log(`${toTitleCase(this.type)} has been successfully updated.`);
        vizality.api.notifications.sendToast({
          icon: this.manifest.icon,
          header: `${toTitleCase(this.type)} \`${this.manifest.name}\` has been successfully updated.`
        });
        await vizality.manager[toPlural(this.type)].remount(this.addonId, false);
      }
      return success;
    } catch (err) {

    }
  }

  /**
   * Enables the file watcher.
   * @private
   */
  async _enableWatcher () {
    /**
     * @note Don't enable the watcher for builtins unless the user is a Vizality developer.
     * No need to use extra resources watching something that shouldn't need it.
     */
    if (!this.manifest) {
      if (!vizality.settings.get('developer', false)) {
        this._watcherEnabled = false;
      } else {
        this._watcherEnabled = vizality.settings.get('hotReload', false);
      }
    } else {
      if (typeof this.manifest.hotReload?.enable === 'boolean') {
        this._watcherEnabled = this.manifest.hotReload.enable;
      } else {
        this._watcherEnabled = vizality.settings.get('hotReload', false);
      }
    }
  }

  /**
   * Disables the file watcher. MUST be called if you no longer need the compiler and the watcher
   * was previously enabled.
   * @private
   */
  async _disableWatcher () {
    this._watcherEnabled = false;
    if (this._watcher?.close) {
      await this._watcher.close();
      this._watcher = {};
    }
  }

  /**
   * @private
   */
  async _watchFiles () {
    const ignored = [];

    /**
     *
     */
    if (this.manifest?.hotReload?.ignore) {
      /**
       *
       */
      if (isArray(this.manifest.hotReload.ignore)) {
        for (const ign of this.manifest.hotReload.ignore) {
          /**
           *
           */
          if (ign.startsWith('*')) {
            ignored.push(ign);
          } else {
            ignored.push(new RegExp(ign));
          }
        }
      } else {
        /**
         *
         */
        if (this.manifest.hotReload.ignore.startsWith('*')) {
          ignored.push(this.manifest.hotReload.ignore);
        } else {
          ignored.push(new RegExp(this.manifest.hotReload.ignore));
        }
      }
    }

    /**
     *
     */
    this._watcher = watch(this.path, {
      ignored: [ /node_modules/, /.git/, /manifest.json/, /.scss/, /.css/ ].concat(ignored),
      ignoreInitial: true
    });

    /**
     * Set up a shorthand for the watcher labels used below.
     */
    const watcherLabels = this._labels.concat({ text: 'Watcher', color: PRESET_LABELS.watcher.toString() });

    /**
     *
     */
    this._watcher
      .on('add', path => this.log(watcherLabels, `File "${path.replace(this.path + sep, '')}" has been added.`))
      .on('change', path => this.log(watcherLabels, `File "${path.replace(this.path + sep, '')}" has been changed.`))
      .on('unlink', path => this.log(watcherLabels, `File "${path.replace(this.path + sep, '')}" has been removed.`))
      .on('addDir', path => this.log(watcherLabels, `Directory "${path.replace(this.path + sep, '')}" has been added.`))
      .on('unlinkDir', path => this.log(watcherLabels, `Directory "${path.replace(this.path + sep, '')}" has been removed.`))
      .on('error', err => this.error(watcherLabels, err))
      .on('all', debounce(async () => vizality.manager[toPlural(this.type)].remount(this.addonId), 300));
  }

  /**
   * @private
   */
  async _load (showLogs = true) {
    try {
      /**
       *
       */
      if (typeof this.start === 'function') {
        const before = performance.now();
        const startPlugin = () => {
          return new Promise(resolve => {
            resolve(this.start());
          });
        };
        startPlugin()
          .then(() => {
            if (showLogs) {
              const after = performance.now();
              const time = parseFloat((after - before).toFixed()).toString().replace(/^0+/, '') || 0;
              /**
               * Let's format the milliseconds to seconds.
               */
              let formattedTime = Math.round((time / 1000 + Number.EPSILON) * 100) / 100;
              /**
               * If it ends up being so fast that it rounds to 0, let's show formatting
               * to 3 decimal places, otherwise show 2 decimal places.
               */
              if (formattedTime === 0) {
                formattedTime = Math.round((time / 1000 + Number.EPSILON) * 1000) / 1000;
              }
              /**
               * If it is still 0, let's just say it's fast.
               */
              if (formattedTime === 0) {
                return this.log(`${toTitleCase(this.type)} loaded. Startup was nearly instant!`);
              }
              return this.log(`${toTitleCase(this.type)} loaded. Startup took ${formattedTime} seconds!`);
            }
          });

        /**
         *
         */
        await this._registerSettings();
      } else {
        this.warn(`${toTitleCase(this.type)} has no "start" method!`);
      }
    } catch (err) {
      return this.error('An error occurred during initialization!', err);
    }

    this._ready = true;
    await this._enableWatcher();
    if (this._watcherEnabled) {
      await this._watchFiles();
    }

    if (Array.isArray(this.manifest?.settings)) {
      const settings = this._mapSettings(this.manifest.settings);
      this.registerSettings(() => jsonToReact(settings, (id, value) => {
        this.settings.set(id, value);
      }));
    }
  }

  /**
   *
   * @private
   */
  async _registerSettings () {
    try {
      /**
       *
       */
      if (!this.sections.settings && this.type !== 'builtin') {
        let Render;
        if (this.manifest?.sections?.settings) {
          Render = await import(join(this.path, this.manifest.sections.settings));
        } else if (existsSync(join(this.path, 'Settings.jsx'))) {
          Render = await import(join(this.path, 'Settings.jsx'));
        } else if (existsSync(join(this.path, 'components', 'Settings.jsx'))) {
          Render = await import(join(this.path, 'components', 'Settings.jsx'));
        }

        /**
         *
         */
        if (Render) {
          vizality.api.settings.registerSettings({
            type: this.type,
            addonId: this.addonId,
            render: Render
          });
        }
      }
    } catch (err) {
      return this.error(err);
    }
  }

  /**
   *
   * @private
   */
  async _registerSections () {
    try {
      [ 'readme', 'changelog' ].forEach(section => {
        /**
         *
         */
        let sectionPath;
        if (this.sections && !this.sections[section]) {
          if (this.manifest?.sections && this.manifest.sections[section]) {
            if (existsSync(join(this.path, this.manifest.sections[section]))) {
              sectionPath = join(this.path, this.manifest.sections[section]);
            }
            /**
             * @todo Possibly allow any case for these file names.
             */
          } else if (existsSync(join(this.path, `${section.toUpperCase()}.md`))) {
            sectionPath = join(this.path, `${section.toUpperCase()}.md`);
          } else if (existsSync(join(this.path, section.toUpperCase()))) {
            sectionPath = join(this.path, section.toUpperCase());
          }

          if (sectionPath) {
            this.sections[section] = readFileSync(sectionPath, 'utf8');
          }
        }
      });

      return;
      /**
       *
       */
      let changelogPath;
      if (!this.sections.changelog) {
        if (this.manifest?.sections?.changelog) {
          if (existsSync(join(this.path, this.manifest.sections.changelog))) {
            changelogPath = join(this.path, this.manifest.sections.changelog);
          }
        } else if (existsSync(join(this.path, 'CHANGELOG.md'))) {
          changelogPath = join(this.path, 'CHANGELOG.md');
        } else if (existsSync(join(this.path, 'CHANGELOG'))) {
          changelogPath = join(this.path, 'CHANGELOG');
        }
      }
      if (changelogPath) {
        this.sections.changelog = readFileSync(changelogPath, 'utf8');
      }

      let readmePath;
      if (!this.sections.overview) {
        if (this.manifest?.sections?.overview) {
          if (existsSync(join(this.path, this.manifest.sections.overview))) {
            readmePath = join(this.path, this.manifest.sections.overview);
          }
        } else if (existsSync(join(this.path, 'README.md'))) {
          readmePath = join(this.path, 'README.md');
        } else if (existsSync(join(this.path, 'README'))) {
          readmePath = join(this.path, 'README');
        }
      }
      if (readmePath) {
        this.sections.overview = readFileSync(readmePath, 'utf8');
      }
    } catch (err) {
      return this.error(err);
    }
  }

  /**
   *
   * @private
   * @param {*} settings
   * @returns
   */
  _mapSettings (settings) {
    return settings.map(setting => {
      if (setting.type === 'category') {
        return {
          ...setting,
          items: this._mapSettings(setting.items)
        };
      }
      if (setting.type === 'divider' || setting.type === 'markdown') {
        return setting;
      }
      return {
        ...setting,
        get value () { return this.settings.get(setting.id, setting.defaultValue); },
        settings: this.settings
      };
    });
  }

  /**
   * @private
   * @param {boolean} showLogs Whether to show console log messages
   */
  async _unload (showLogs = true) {
    try {
      for (const id in this.styles) {
        this.styles[id].compiler.on('src-update', this.styles[id].compile);
        this.styles[id].compiler.disableWatcher();
        if (document.getElementById(`${this.type}-${this.addonId}-${id}`)) {
          document.getElementById(`${this.type}-${this.addonId}-${id}`).remove();
        }
      }

      this.styles = {};
      if (typeof this.stop === 'function') {
        await this.stop();
      }

      if (this.type !== 'builtin') {
        // Unregister settings
        if (this.sections.settings) {
          vizality.api.settings.unregisterSettings(this.addonId, 'plugin');
        }

        unpatchAll(this.addonId);
      }

      if (showLogs) {
        this.log(`${toTitleCase(this.type)} unloaded!`);
      }
    } catch (err) {
      this.error(`An error occurred while shutting down! It's heavily recommended that you reload Discord to ensure there are no conflicts.`, err);
    } finally {
      this._ready = false;
      /**
       * Disable any leftover watcher.
       */
      if (this._watcher) {
        await this._disableWatcher();
      }
      /**
       * Remove any event listeners added to the plugin.
       */
      this.removeAllListeners();
    }
  }
}
