const Os = require('os'),
  Path = require('path'),
  Fsx = require('fs-extra'),
  _ = require('lodash'),
  ChildProcessPromise = require('child-process-promise'),
  Tmp = require('tmp'),
  { stopwatch } = require('durations'),
  Events = require('events'),
  Semaphore = require('semaphore');

class ScriptBase extends Events.EventEmitter {
  constructor(options = {}) {
    super();

    this.options = _.defaultsDeep(options, {
      name: 'Unknown',
      spawnOptions: {
        timeout: 10 * 1000,
      },
    });

    this.tmps = [];
    this.binDirPath = options.binDirPath;

    this._phase = '';
  }

  phase(name) {
    if (name) this._phase = name;
    return this._phase;
  }

  static async install(path) {
    throw new Error('override');
  }

  static commandFileName(command) {
    if (Os.platform() == 'win32' && !command.match(/\.exe$/)) {
      command = command + '.exe';
    }
    return command;
  }

  static async installCommandFiles(installDir, commands = []) {
    await Promise.all(commands.map(async c => {
      const commandPath = this.commandFilePath(c);
      const installPath = Path.join(installDir, this.commandFileName(c));

      // コマンドが階層である可能性に考慮
      const dirPath = Path.dirname(installPath);
      if (await Fsx.exists(dirPath)) Fsx.mkdirp(dirPath);

      // Fsx.copyではpkg内でエントリを発見できないため、read/write/chmodする
      const buffer = await Fsx.readFile(commandPath);
      await Fsx.writeFile(installPath, buffer);
      await Fsx.chmod(installPath, '755');
    }));
  }

  static commandFilePath(name) {
    return Path.join(__dirname, 'bin', Os.platform(), Os.arch(), this.commandFileName(name));
  }

  commandFilePath(name) {
    if (this.binDirPath) return Path.join(this.binDirPath, this.constructor.commandFileName(name));
    return this.constructor.commandFilePath(name);
  }

  newTmpFile(options = {}) {
    _.defaults(options, { unsafeCleanup: true });
    const tmp = Tmp.fileSync(options);
    this.tmps.push(tmp);
    return tmp;
  }

  newTmpDir(options = {}) {
    _.defaults(options, { unsafeCleanup: true });
    const tmp = Tmp.dirSync(options);
    this.tmps.push(tmp);
    return tmp;
  }

  async cleanup() {
    this.tmps.forEach(tmp => tmp.removeCallback());
  }

  fileCompressMetric(op, beforeStat, afterStat, duration) {
    const values = {
      op: op,
    };
    if (beforeStat) values.before = beforeStat.size;
    if (afterStat) values.after = afterStat.size;
    if (duration) values.duration = duration.millis();
    if (values.before > 0 && values.after > 0) {
      values.reduced = values.before - values.after;
      values.rate = values.after * 1.0 / values.before;
    }
    return values;
  }

  async executeCommand(commandName, args = [], options = {}) {
    // ラベル
    const label = options.label || commandName;

    // コマンドのパス
    const commandPath = this.commandFilePath(commandName);
    const result = await ScriptBase.concurrent(async() => {
      return await ChildProcessPromise.spawn(commandPath, args, { capture: ['stdout', 'stderr'] })
    });
    return result;
  }

  async readCommentLightFileMeta(path) {
    throw new Error('Must override');
  }

  async run(...args) {
    try {
      const sw = stopwatch();
      sw.start();

      const path = args[0];
      const beforeStat = await Fsx.stat(path);
      const metrics = await this.runScript(...args);
      const afterStat = await Fsx.stat(path);

      _.merge(metrics, {
        before: {
          size: beforeStat.size,
        },
        after: {
          size: afterStat.size,
        },
        reduced: {
          size: beforeStat.size - afterStat.size,
        },
        duration: sw.duration().millis(),
      })

      return metrics;
    } catch (ex) {
      if (ex.message) ex.message = `${this.phase()}; ${ex.message}`;
      throw ex;
    } finally {
      try {
        this.cleanup();
      } catch (ex) {}
    }
  }

  async runScript(...args) {
    throw new Error('must override');
  }

  static runnableFor(path) {
    return false;
  }
}

ScriptBase.options = {
  maxProcesses: Os.cpus().length
};

ScriptBase.basePath = Path.join(__dirname, 'bin');
ScriptBase.currentCoucurrency = ScriptBase.maxConcurrency = 0;

ScriptBase.concurrent = async(cb) => {
  if (!ScriptBase.semaphore) ScriptBase.semaphore = new Semaphore(ScriptBase.options.maxProcesses);

  return new Promise((resolve, reject) => {
    ScriptBase.semaphore.take(async() => {
      try {
        ScriptBase.currentCoucurrency++;
        if (ScriptBase.maxConcurrency < ScriptBase.currentCoucurrency) ScriptBase.maxConcurrency = ScriptBase.currentCoucurrency
        const result = await cb();
        resolve(result);
      } catch (ex) {
        reject(ex);
      } finally {
        ScriptBase.currentCoucurrency--;
        ScriptBase.semaphore.leave();
      }
    });
  });
};

module.exports = {
  ScriptBase: ScriptBase,
};