"use strict";

const util = require("util");
const assert = require("assert");
const crypto = require("crypto");
const fs = require("fs");
const path = require("path");
const { rollup } = require("rollup");

const readFile = util.promisify(fs.readFile);
const writeFile = util.promisify(fs.writeFile);

const ROOT = path.join(__dirname, "..", "..");

function Cache(cacheDir, version) {
  this.cacheDir = path.resolve(cacheDir || required("cacheDir"));
  this.manifest = path.join(this.cacheDir, "manifest.json");
  this.version = version || required("version");
  this.checksums = {};
  this.files = {};
  this.updated = {
    version: this.version,
    checksums: {},
    files: {}
  };
}

// Loads the manifest.json file with the information from the last build
Cache.prototype.load = async function() {
  // This should never throw, if it does, let it fail the build
  const lockfile = await readFile("yarn.lock", "utf-8");
  const lockfileHash = hashString(lockfile);
  this.updated.checksums["yarn.lock"] = lockfileHash;

  try {
    const manifest = await readFile(this.manifest, "utf-8");
    const { version, checksums, files } = JSON.parse(manifest);

    // Ignore the cache if the version changed
    assert.equal(this.version, version);

    assert.ok(typeof checksums === "object");
    // If yarn.lock changed, rebuild everything
    assert.equal(lockfileHash, checksums["yarn.lock"]);
    this.checksums = checksums;

    assert.ok(typeof files === "object");
    this.files = files;

    for (const files of Object.values(this.files)) {
      assert.ok(Array.isArray(files));
    }
  } catch (err) {
    this.checksums = {};
    this.files = {};
  }
};

// Run rollup to get the list of files included in the bundle and check if
// any (or the list itself) have changed.
// This takes the same rollup config used for bundling to include files that are
// resolved by specific plugins.
Cache.prototype.checkBundle = async function(id, inputOptions, outputOptions) {
  const files = new Set(this.files[id]);
  const newFiles = (this.updated.files[id] = []);

  let dirty = false;

  const bundle = await rollup(getRollupConfig(inputOptions));
  const { output } = await bundle.generate(outputOptions);

  const modules = output
    .filter(mod => !/\0/.test(mod.facadeModuleId))
    .map(mod => [path.relative(ROOT, mod.facadeModuleId), mod.code]);

  for (const [id, code] of modules) {
    newFiles.push(id);
    // If we already checked this file for another bundle, reuse the hash
    if (!this.updated.checksums[id]) {
      this.updated.checksums[id] = hashString(code);
    }
    const hash = this.updated.checksums[id];

    // Check if this file changed
    if (!this.checksums[id] || this.checksums[id] !== hash) {
      dirty = true;
    }

    // Check if this file is new
    if (!files.delete(id)) {
      dirty = true;
    }
  }

  // Final check: if any file was removed, `files` is not empty
  return !dirty && files.size === 0;
};

Cache.prototype.save = async function() {
  try {
    await writeFile(this.manifest, JSON.stringify(this.updated, null, 2));
  } catch (err) {
    // Don't fail the build
  }
};

function required(name) {
  throw new Error(name + " is required");
}

function hashString(string) {
  return crypto
    .createHash("md5")
    .update(string)
    .digest("hex");
}

function getRollupConfig(rollupConfig) {
  return Object.assign({}, rollupConfig, {
    onwarn() {},
    plugins: rollupConfig.plugins.filter(
      plugin =>
        // We're not interested in dependencies, we already check `yarn.lock`
        plugin.name !== "node-resolve" &&
        // This is really slow, we need this "preflight" to be fast
        plugin.name !== "babel"
    )
  });
}

module.exports = Cache;
