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

import { initialize as initializeWebpackModules, getModule } from '@vizality/webpack';
import { log as _log, warn as _warn, error as _error } from '@vizality/util/logger';
import { Directories, Developers, Events } from '@vizality/constants';
import { resolveCompiler } from '@vizality/compilers';
import { createElement } from '@vizality/util/dom';
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._initialized = false;
    this._patchWebSocket();

    /**
     * Wait for the document to be loaded and then initialize Vizality.
     * @todo There should be a better way to do this, look into it more in the future.
     */
    if (document.readyState === 'loading') {
      document.addEventListener('DOMContentLoaded', () => this.initialize());
    } else {
      this.initialize();
    }
  }

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

      /**
       * 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 =>
        require('@vizality/components').AsyncComponent.from((async () => {
          const awaitedStores = await Promise.all(stores);
          // @todo Remember to add these to @discord/settings module (darkSiderbar, etc.): awaitedStores
          return Flux.connectStores(awaitedStores, props => fn(awaitedStores, props))(Component);
        })());

      /**
       * Instantiate the managers.
       * @note We're doing this down here, and uglily so we can use webpack modules and
       * Vizality's components module inside the managers.
       */
      this.manager = {};
      const CommunityManager = require('./managers/Community').default;
      const BuiltinManager = require('./managers/Builtin').default;
      const PluginManager = require('./managers/Plugin').default;
      const ThemeManager = require('./managers/Theme').default;
      const APIManager = require('./managers/API').default;
      this.manager.community = new CommunityManager();
      this.manager.builtins = new BuiltinManager();
      this.manager.plugins = new PluginManager();
      this.manager.themes = new ThemeManager();
      this.manager.apis = new APIManager();

      /**
       * Time to start Vizality.
       */
      const before = performance.now();
      const startVizality = () => {
        return new Promise(resolve => {
          resolve(this.start());
        });
      };
      await startVizality()
        .then(() => {
          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;
          }
          return this.log(`Vizality loaded. Startup took ${formattedTime} seconds!`);
        });

      /**
       * 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());
      }

      /**
       * 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('vizality://assets/images/console-banner.gif') no-repeat center / contain; padding: 110px 350px; font-size: 1px; margin: 10px 0;`);

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

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

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

    /**
     * Check if the current user is a verified Vizality Developer.
     */
    const currentUserId = (await import('@discord/user')).getCurrentUser()?.id;
    if (Developers.includes(currentUserId)) {
      this.settings.set('vizalityDeveloper', true);
    }

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

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

    /**
     * Set up the modules for the global vizality object.
     */
    this.modules = {};
    const modules = await import('@vizality/modules');
    for (const mdl of Object.keys(modules)) {
      Object.assign(this.modules, { [mdl]: modules[mdl] });
    }

    /**
     * Set up a shorthand vizality global object with the namespace $vz.
     */
    window.$vz = Object.assign({}, this.manager, this.api, this.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 discordModule = await import('@discord');
      window.discord = Object.assign({}, discordModule);
      delete window.discord.default;
    }

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

  /**
   * 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();
    this._initialized = false;
  }

  /**
   * 
   */
  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: 'vizality://assets/images/discord.png', labels: [ 'DiscordNative', submodule ], message });
            case 'error':
            case 'trace':
              return _error({ badge: 'vizality://assets/images/discord.png', labels: [ 'DiscordNative', submodule ], message });
            case 'warn':
              return _warn({ badge: 'vizality://assets/images/discord.png', labels: [ 'DiscordNative', submodule ], message });
            default:
              return _log({ badge: 'vizality://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('vizalityDeveloper', 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 
   * @returns {void}
   */
  log (...message) {
    return _log({ labels: this._labels, message });
  }

  /**
   * 
   * @param  {...any} message 
   * @returns {void}
   */
  warn (...message) {
    return _warn({ labels: this._labels, message });
  }

  /**
   * 
   * @param  {...any} message 
   * @returns {void}
   */
  error (...message) {
    return _error({ labels: this._labels, message });
  }
}
