const fs = require('fs');
const path = require('path');
const { createHash } = require("crypto");
const Module = module.constructor;

module.exports = function enable ({ cachePath, createLoader = true } = {}) {
  let cacheEnabled = !!cachePath;
  let cacheEntries = Object.create(null);

  if (cachePath) {
    try {
      fs.readdirSync(cachePath).forEach(name => {
        cacheEntries[name] = true;
      });
    } catch (e) {
      if (e.code === 'ENOENT') {
        fs.mkdirSync(cachePath);
      } else {
        cacheEnabled = false;
      }
    }
  }

  const Mp = Module.prototype;

  Mp.resolve = function (id) {
    return Module._resolveFilename(id, this);
  };

  // Enable the module.{watch,export,...} runtime API needed by Reify.
  require("@meteorjs/reify/lib/runtime").enable(Mp);

  const moduleLoad = Mp.load;
  Mp.load = function (filename) {
    const result = moduleLoad.apply(this, arguments);
    if (typeof this.runSetters === "function") {
      // Make sure we call module.runSetters (or module.runModuleSetters, a
      // legacy synonym) whenever a module finishes loading.
      this.runSetters();
    }
    return result;
  };

  const resolved = Promise.resolve();
  Mp.dynamicImport = function (id) {
    return resolved.then(() => require(id));
  };

  const reifyVersion = require("@meteorjs/reify/package.json").version;
  const reifyAcornParse = require("@meteorjs/reify/lib/parsers/acorn").parse;
  const reifyBabelParse = require("@meteorjs/reify/lib/parsers/babel").parse;
  const reifyCompile = require("@meteorjs/reify/lib/compiler").compile;

  function compileContent (content) {
    let identical = true;

    let result;
    try {
      result = reifyCompile(content, {
        parse: reifyAcornParse,
        generateLetDeclarations: false,
        ast: false,
      });
      if (!result.identical) {
        identical = false;
        content = result.code;
      }
    } catch (acornError) {
      // Fallback: Babel
      result = reifyCompile(content, {
        parse: reifyBabelParse,
        generateLetDeclarations: false,
        ast: false,
      });
      if (!result.identical) {
        identical = false;
        content = result.code;
      }
    }

    return { content, identical };
  }

  const _compile = Mp._compile;
  Mp._compile = function (content, filename, options) {
    // When cache is enabled, the file has already been compiled
    if (!options || !options.compiledWithReify) {
      content = compileContent(content).content;
    }

    return _compile.call(this, content, filename);
  };

  if (cacheEnabled) {
    const jsExt = Module._extensions.js;
    Module._extensions['.js'] = function (module, filename) {
      let stat = fs.statSync(filename);
      let baseKey = createHash("sha1")
        .update(`${reifyVersion}\0${filename}\0${stat.mtimeMs}\0${stat.ino}\0${stat.size}\0`)
        .digest('hex');

      // When files don't use import/export, there is no reason to store
      // an identical copy of the file in the cache. Instead, it stores an empty
      // file with a different suffix to indicate the original file should be used
      let identicalKey = baseKey + '-identical.json';
      let key = baseKey + '.json';

      let content;
      if (cacheEntries[key]) {
        content = fs.readFileSync(path.join(cachePath, key), 'utf8');
      } else if (cacheEntries[identicalKey]) {
        content = fs.readFileSync(filename, 'utf8');
      } else {
        let origContent = fs.readFileSync(filename, 'utf8');
        let result = compileContent(origContent);
        content = result.content;

        if (result.identical) {
          writeFileLater(identicalKey, '');
        } else {
          writeFileLater(key, content);
        }
      }

      return module._compile(content, filename, { compiledWithReify: true });
    }
  }

  let immediateTimer = null;
  let pendingWrites = Object.create(null);
  function writeFileLater(key, content) {
    pendingWrites[key] = content;
    if (immediateTimer !== null) {
      return;
    }

    immediateTimer = setImmediate(() => {
      immediateTimer = null;
      Object.keys(pendingWrites).forEach(key => {
        try {
          let targetPath = path.resolve(cachePath, key);
          let tempPath = targetPath + '.tmp';
          fs.writeFileSync(tempPath, pendingWrites[key]);
          fs.renameSync(tempPath, targetPath);
        } catch (err) {
        }
      });

      pendingWrites = Object.create(null);
    });
  }
}
