const { readFileSync, writeFileSync, existsSync, mkdirSync } = require('fs');
const { createHash } = require('crypto');
const { debounce } = require('lodash');
const { watch } = require('chokidar');
const Events = require('events');
const { join } = require('path');


// @todo: Schedule a cache cleanup?

/**
 * Main class for compilers used in Vizality.
 * If using the watcher, MAKE SURE TO DISPOSE OF THE COMPILER PROPERLY. You **MUST** disable
 * the watcher if you no longer need the compiler. When watch events are emitted, the compiler
 * should be re-used if a recompile is performed.
 * @property {string} file File to compile
 * @property {string} cacheDir Path where cached files will go
 * @property {string} watcherEnabled Whether the file watcher is enabled or not
 * @abstract
 */
module.exports = class Compiler extends Events {
  constructor (file) {
    super();
    this.file = file;
    this.cacheDir = join(__dirname, '..', '..', '..', '.cache', this.constructor.name.toLowerCase());
    this.watcherEnabled = false;
    this._watchers = {};
    this._compiledOnce = {};
    this._labels = [ 'Compiler', this.constructor.name ];
    if (!existsSync(this.cacheDir)) {
      mkdirSync(this.cacheDir, { recursive: true });
    }
  }

  /**
   * Enables the file watcher. Will emit "src-update" event if any of the files are updated.
   */
  enableWatcher () {
    this.watcherEnabled = true;
  }

  /**
   * Disables the file watcher. MUST be called if you no longer need the compiler and the watcher
   * was previously enabled.
   */
  disableWatcher () {
    this.watcherEnabled = false;
    Object.values(this._watchers).forEach(w => w.close());
    this._watchers = {};
  }

  /**
   * Compiles the file (if necessary), and perform cache-related operations.
   * @returns {Promise<string>|string} Compilation result
   */
  compile () {
    try {
      // Attemt to fetch from cache
      const cacheKey = this.computeCacheKey();
      if (cacheKey instanceof Promise) {
        return cacheKey.then(key => this._doCompilation(key));
      }
      return this._doCompilation(cacheKey);
    } catch (err) {
      return console.log('pizza', err);
    }
  }

  /**
   * @private
   */
  _doCompilation (cacheKey) {
    try {
      let cacheFile;
      if (cacheKey) {
        cacheFile = join(this.cacheDir, cacheKey);
        if (existsSync(cacheFile)) {
          const compiled = readFileSync(cacheFile, 'utf8');
          this._finishCompilation(null, compiled);
          return compiled;
        }
      }
      /*
       * Perform compilation.
       */
      const compiled = this._compile();
      if (compiled instanceof Promise) {
        return compiled.then(finalCompiled => {
          this._finishCompilation(cacheFile, finalCompiled);
          return finalCompiled;
        });
      }
      this._finishCompilation(cacheFile, compiled);
      return compiled;
    } catch (err) {
      return this._error(err);
    }
  }

  /**
   * @private
   */
  _finishCompilation (cacheFile, compiled) {
    try {
      if (cacheFile) {
        writeFileSync(cacheFile, compiled, () => void 0);
      }
      if (this.watcherEnabled) {
        this._watchFiles();
      }
    } catch (err) {
      // Triggered when you delete cache (on startup only maybe)
      return console.log('fishing');
    }
  }

  /**
   * @private
   */
  async _watchFiles () {
    try {
      const files = await this.listFiles();
      /*
       * Filter no longer used watchers.
       */
      Object.keys(this._watchers).forEach(file => {
        if (!files.includes(file)) {
          this._watchers[file].close();
          delete this._watchers[file];
        }
      });
      /*
       * Add new watchers.
       */
      files.forEach(file => {
        if (!this._watchers[file]) {
          this._watchers[file] = watch(file);
          this._watchers[file].on('all', debounce(async () => this.emit('src-update'), 300));
        }
      });
    } catch (err) {
      return console.log('fishing');
    }
  }

  /**
   * Lists all files involved during the compilation (parent file + imported files).
   * Only applicable if files are concatenated during compilation (e.g. scss files).
   * @returns {Promise<string[]>|string[]}
   */
  listFiles () {
    try {
      return [ this.file ];
    } catch (err) {
      return console.log('fishing');
    }
  }

  /**
   * Computes the hash corresponding to the file we're compiling.
   * MUST take into account imported files (if any) and always return the same hash for the same given file.
   * @returns {Promise<string|null>|string|null} Cache key, or null if cache isn't available
   */
  computeCacheKey () {
    try {
      const files = this.listFiles();
      if (files instanceof Promise) {
        return files.then(this._computeCacheKey.bind(this));
      }
      return this._computeCacheKey(files);
    } catch (err) {
      return console.log('fishing');
    }
  }

  /** @private */
  _computeCacheKey (files) {
    try {
      const hashes = files.map(this.computeFileHash.bind(this));
      if (hashes.length === 1) {
        return hashes[0];
      }
      const hash = createHash('sha1');
      hashes.forEach(h => hash.update(h));
      return hash.digest('hex');
    } catch (err) {
      return console.log('fishing');
    }
  }

  /**
   * Computes the hash of a given file.
   * @param {string} file File path
   */
  computeFileHash (file) {
    try {
      if (!existsSync(file)) {
        throw new Error('File doesn\'t exist!');
      }
      const fileBuffer = readFileSync(file);
      return createHash('sha1')
        .update(this._metadata)
        .update(fileBuffer)
        .digest('hex');
    } catch (err) {
      return console.log('fishing');
    }
  }

  /**
   * Compiles the file. Should NOT perform any cache-related actions.
   * @returns {Promise<string>} Compilation results.
   */
  _compile () {
    throw new Error('Not implemented');
  }

  /**
   * @returns {string} Compiler metadata (compiler used, version)
   */
  get _metadata () {
    return '';
  }

  /**
   * 
   * @param  {...any} message 
   * @private
   */
  _log (...message) {
    return require('@vizality/util/logger').log({ labels: this._labels, message });
  }

  /**
   * 
   * @param  {...any} message 
   * @private
   */
  _warn (...message) {
    return require('@vizality/util/logger').warn({ labels: this._labels, message });
  }

  /**
   * 
   * @param  {...any} message 
   * @private
   */
  _error (...message) {
    return require('@vizality/util/logger').error({ labels: this._labels, message });
  }
};
