/**
 *
 * @module Vizality
 * @namespace Vizality
 */

import { log as _log, warn as _warn, error as _error, deprecate as _deprecate } from '@vizality/util/logger';
import { initialize as initializeWebpackModules, getModule, FluxDispatcher, getAllModules } from '@vizality/webpack';
import { Directories, Events, Protocols, Developers } from '@vizality/constants';
import { resolveCompiler } from '@vizality/compilers';
import { createElement } from '@vizality/util/dom';
import { toPlural } from '@vizality/util/string';
import { isArray } from '@vizality/util/array';
import { Updatable } from '@vizality/entities';
import { debounce } from 'lodash';
import { promisify } from 'util';
import cp from 'child_process';
import { promises } from 'fs';
import { join } from 'path';

const { readFile, writeFile } = promises;
const exec = promisify(cp.exec);

/**
 * @extends Updatable
 * @extends Events
 */
export default class Vizality extends Updatable {
  constructor () {
    super(Directories.ROOT, '', 'vizality');
    /**
     * This is a leftover prop from Updatable, not needed for Vizality's core.
     */
    delete this.addonId;

    /**
     * @note Copy over VizalityNative to vizality.native and then delete
     * VizalityNative, so that we are staying consistent and only have
     * one top level global variable.
     */
    this.native = window.VizalityNative;
    delete window.VizalityNative;

    /**
     * Set up the git info, defaulting to ???
     */
    this.git = {
      upstream: '???',
      branch: '???',
      revision: '???'
    };

    /**
     * Labels used for console logging purposes.
     */
    this._labels = [ 'Vizality', 'Core' ];

    this.initialize();
  }

  async handleConnectionOpen () {
    return new Promise(resolve => {
      console.log(getAllModules()?.length);
      if (getAllModules()?.length > 7000) {
        return resolve();
      }
      FluxDispatcher.subscribe('CONNECTION_OPEN', () => resolve());
    });
  }

  async ensureWebpackModules () {
    try {
      /**
       * Initialize the webpack modules.
       */
      await initializeWebpackModules();

      /**
       * Set up a connectStoresAsync FLux method.
       * @note This has to be after webpack modules have been initialized.
       */
      const Flux = await getModule('Store', 'PersistedStore', true);
      Flux.connectStoresAsync = (stores, fn) => Component =>
        import('./components').AsyncComponent.from((async () => {
          const awaitedStores = await Promise.all(stores);
          return Flux.connectStores(awaitedStores, props => fn(awaitedStores, props))(Component);
        })());

      await this.handleConnectionOpen();
    } catch (err) {
      this.error(`Something went wrong while initializing webpack modules: ${err}`);
    }
  }

  /**
   * Initialize Vizality.
   */
  async initialize () {
    try {
      await this.ensureWebpackModules();

      /**
       * Get rid of Discord's "Hold Up" dev tools warning.
       */
      DiscordNative?.window?.setDevtoolsCallbacks(null, null);

      /**
       * Instantiate the managers.
       * @note We're doing this down here so that we can utilize webpack modules and
       * components inside of the managers.
       */
      this.manager = {};
      const managers = [ 'API', 'Builtin', 'Plugin', 'Theme', 'Community' ];
      for (const manager of managers) {
        /**
         * Make the manager names on the global object plural, except for Community.
         */
        const formatted = manager === 'Community' ? manager : toPlural(manager);
        this.manager[formatted.toLowerCase()] = new (await import(`./managers/${manager}`))();
      }

      await this.start();

      /**
       * Get and assign the newly updated git info.
       */
      this.git = await this.manager.builtins.get('updater')?.getGitInfo();

      /**
       * Token manipulation stuff. Helps prevent unwanted logouts.
       */
      if (this.settings.get('hideToken', true)) {
        const tokenModule = getModule('hideToken');
        tokenModule.hideToken = () => void 0;
        setImmediate(() => tokenModule.showToken());
      }

      /**
       * Enables/disables Discord Experiments.
       */
      if (this.settings.get('discordExperiments', false)) {
        const experimentsModule = getModule(user => user.isDeveloper !== void 0);
        Object.defineProperty(experimentsModule, 'isDeveloper', {
          get: () => true,
          configurable: true
        });
      }

      /**
       * Trigger an event indicating that Vizality has been initialized.
       */
      this.emit(Events.VIZALITY_READY);
    } catch (err) {
      return this.error(err);
    }
  }

  /**
   * Starts up the core functionality of Vizality, including APIs, builtins, plugins, and themes.
   */
  async start () {
    /**
     * Clean up console by clearing it first, then log our startup banner.
     */
    console.clear();
    console.log('%c ', `background: url('${Protocols.ASSETS}/images/console-banner.gif') no-repeat center / contain; padding: 110px 350px; font-size: 1px; margin: 10px 0;`);

    /**
     * Set up the modules for the global vizality object.
     */
    this.modules = {};
    const modules = await import('./modules');
    Object.assign(this.modules, modules);

    /**
     * Set up a shorthand for Vizality's Discord module.
     * Make sure it doesn't exist already, just in case Discord ever uses the same global namespace itself.
     */
    if (!window.$discord) {
      const discord = await import('./modules/discord');
      window.$discord = Object.assign({}, discord);
      window.$discord.constants = Object.assign({}, { ...window.$discord.constants, ...window.$discord.constants.default });
    }

    /**
     * Perform some cleanup, removing unnecessary properties.
     */
    delete this.modules.discord;
    delete window.$discord.constants.default;
    delete window.$discord.default;

    this.deprecate('The global namespace object "discord" will be removed on May 24, 2022. Please use "$discord" instead.');
    this.deprecate('The global namespace object "vizality" will be removed on May 24, 2022. Please use "$vz" instead. "$vz" has a slightly different data structure, but provides the same information.');

    /**
     * Initialize the APIs.
     */
    this.api = {};
    await this.manager.apis.initialize();

    /**
     * Set up a shorthand vizality global object with the namespace $vz.
     */
    window.$vz = Object.assign({}, this.manager, this.modules);

    window.$vz.api = this.api;

    /**
     * Set up and initialize Vizality's core settings.
     */
    this.settings = this.api.settings._buildCategoryObject('settings');

    window.$vz.settings = this.settings;

    this.settings.set('developer', false);

    /**
     * Check if the current user is a Vizality Developer.
     * @note This is going before the settings ready event below, because we check this in
     * the Icon component after settings ready event has triggered.
     */

    /*
     * if (Developers.some(developer => developer.id === $discord.users.getCurrentUser()?.id)) {
     *   console.log('yes');
     *   this.settings.set('developer', true);
     * } else {
     *   this.settings.set('developer', false);
     * }
     */

    /**
     * Trigger an event indicating that Vizality's settings are ready.
     */
    this.emit(Events.VIZALITY_SETTINGS_READY);

    /**
     * Inject core Vizality styles.
     */
    this._injectCoreStyles();

    /**
     * Patch Discord's stylized console logs.
     * @note This has to be after settings have been initialized.
     */
    this._patchDiscordLogs();

    /**
     * Initialize builtins, plugins, and themes.
     */
    await this.manager.builtins.initialize();
    this.manager.themes.initialize();
    this.manager.plugins.initialize();
    this.manager.community.initialize();
  }

  /**
   * Shuts down Vizality's APIs, builtins, plugins, and themes.
   */
  async stop () {
    /**
     * Most importantly here is to stop entities in the order of plugins -> builtins -> apis
     * to ensure there aren't any problems shutting down something that relies on
     * something else.
     */
    this.manager.themes.stop();
    this.manager.plugins.stop();
    await this.manager.builtins.stop();
    await this.manager.apis.stop();
  }

  /**
   *
   */
  async _patchDiscordLogs () {
    try {
      const { setLogFn } = await getModule('setLogFn', true);
      if (!this.settings.get('showDiscordConsoleLogs', false)) {
        /**
         * Removes Discord's logs entirely... except for the logs that don't use the
         * setLogFn function (i.e. normal console methods).
         */
        setLogFn(() => void 0);
      } else {
        /**
         * Patch Discord's logs to adhere to Vizality's log styles.
         */
        setLogFn((submodule, type, ...message) => {
          switch (type) {
            case 'info':
            case 'log':
              return _log({ badge: `${Protocols.ASSETS}/images/discord.png`, labels: [ 'DiscordNative', submodule ], message });
            case 'error':
            case 'trace':
              return _error({ badge: `${Protocols.ASSETS}/images/discord.png`, labels: [ 'DiscordNative', submodule ], message });
            case 'warn':
              return _warn({ badge: `${Protocols.ASSETS}/images/discord.png`, labels: [ 'DiscordNative', submodule ], message });
            default:
              return _log({ badge: `${Protocols.ASSETS}/images/discord.png`, labels: [ 'DiscordNative', submodule ], message });
          }
        });
      }
    } catch (err) {
      return this.error(err);
    }
  }

  /**
   *
   */
  _patchWebSocket () {
    const _this = this;
    window.WebSocket = class PatchedWebSocket extends window.WebSocket {
      constructor (url) {
        super(url);
        this.addEventListener('message', data => {
          _this.emit(`webSocketMessage:${data.origin.slice(6)}`, data);
        });
      }
    };
  }

  /**
   *
   * @param {boolean} [force=false] Whether to
   * @returns {Promise<boolean>}
   */
  async _update (force = false) {
    try {
      const success = await super._update(force);
      /**
       *
       */
      if (success) {
        try {
          await exec('npm install --only=prod --legacy-peer-deps', { cwd: this.dir });
        } catch (err) {
          return this.error(`An error occurred while updating Vizality's dependencies!`, err);
        }
        /**
         *
         */
        if (!document.querySelector(`#vz-updater-update-complete, [vz-route='updater']`)) {
          this.api.notifications.sendToast({
            id: 'VIZALITY_UPDATER_UPDATE_COMPLETE',
            header: 'Update complete!',
            content: `Please click 'Reload' to complete the final stages of this Vizality update.`,
            icon: 'CloudDone',
            buttons: [
              {
                text: 'Reload',
                color: 'green',
                look: 'ghost',
                onClick: () => DiscordNative?.app?.relaunch()
              },
              {
                text: 'Postpone',
                color: 'grey',
                look: 'outlined',
                onClick: () => this.api.notifications.closeToast('VIZALITY_UPDATER_UPDATE_COMPLETE')
              }
            ]
          });
        }
        this.manager.builtins.get('updater').settings.set('awaitingReload', true);
      }
      return success;
    } catch (err) {
      return this.error(`An error occurred while updating Vizality!`, err);
    }
  }

  /**
   * Injects a style element containing Vizality's core styles.
   * @note Includes a file watcher and CSS file generator only for Vizality developers.
   * @returns {Promise<void>}
   */
  async _injectCoreStyles () {
    try {
      const id = 'vizality-core-styles';
      /**
       * Check if the user is a Vizality developer.
       */
      if (this.settings.get('developer', false)) {
        const path = join(Directories.STYLES, 'main.scss');
        const compiler = resolveCompiler(path);
        const style = createElement('style', { id, 'vz-style': '' });
        document.head.appendChild(style);

        /**
         * Compiles the Sass and then writes it into a main.css file.
         * @param {boolean} [showLogs=true] Whether to show log messages
         * @returns {Promise<void>}
         */
        const compile = debounce(async (showLogs = true) => {
          try {
            const before = performance.now();
            const compiled = await compiler.compile();
            style.innerHTML = compiled;
            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) {
                this.log(`Core styles compiled. Compilation was nearly instant!`);
              } else {
                this.log(`Core styles compiled. Compilation took ${formattedTime} seconds!`);
              }
            }
            await writeFile(join(Directories.STYLES, 'main.css'), compiled);
          } catch (err) {
            return this.error(err);
          }
        }, 300);

        /**
         * Set up the watcher for the compiler.
         */
        compiler.enableWatcher();
        compiler.on('src-update', compile);
        this[`__compileStylesheet_${id}`] = compile;
        this[`__compiler_${id}`] = compiler;
        return compile(false);
      }

      /**
       * Create a style element and inject the CSS into it.
       */
      const path = join(__dirname, 'styles', 'main.css');
      const style = createElement('style', { id, 'vz-style': '' });
      document.head.appendChild(style);
      const css = await readFile(path, 'utf8');
      style.innerHTML = css;
    } catch (err) {
      return this.error(err);
    }
  }

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

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

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

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