#!/usr/bin/env node

const fs = require("fs");
const path = require("path");
const parse = require("../parser").default;
const JSZip = require("jszip");
const { getClass, getFunctionObject, getFormattedId } = require("../objects");
const glob = require("glob");

const CALL_OPCODES = new Set([24, 112]);
const SKIN_FILENAME_BLACKLIST = new Set(["Fuzzy_Muffins.wal"]);

function findWals(parentDir) {
  return new Promise((resolve, reject) => {
    glob("**/*.wal", { cwd: parentDir }, (err, files) => {
      if (err) {
        return reject(err);
      }
      resolve(files.map(filePath => path.join(parentDir, filePath)));
    });
  });
}

function sumCountObjects(obj1, obj2) {
  return Object.keys(obj2).reduce((summaryObj, key) => {
    if (summaryObj[key] == null) {
      summaryObj[key] = obj2[key];
    } else {
      summaryObj[key] += obj2[key];
    }
    return summaryObj;
  }, Object.assign({}, obj1));
}

async function getCallCountsFromWal(absolutePath) {
  const buffer = fs.readFileSync(absolutePath);
  const zip = await JSZip.loadAsync(buffer);
  const files = zip.file(/\.maki$/);
  const buffers = await Promise.all(
    files.map(file => file.async("nodebuffer"))
  );
  return buffers.map(getCallCountsFromMaki).reduce(sumCountObjects, {});
}

function getCallCountsFromMaki(buffer) {
  const maki = parse(buffer);
  return maki.commands
    .filter(command => CALL_OPCODES.has(command.opcode))
    .map(command => {
      const method = maki.methods[command.arg];
      const classId = maki.classes[method.typeOffset];
      const klass = getClass(classId);
      if (klass == null) {
        throw new Error(
          `Unknown class ID: ${getFormattedId(classId)} aka ${classId} (raw)`
        );
      }
      const parentClass = getFunctionObject(klass, method.name);
      return `${parentClass.name}.${method.name.toLowerCase()}`;
    })
    .map(methodName => ({ [methodName]: 1 }))
    .reduce(sumCountObjects, {});
}

function setObjectValuesToOne(obj) {
  const newObj = {};
  Object.keys(obj).forEach(key => {
    if (obj[key] != null) {
      newObj[key] = 1;
    }
  });

  return newObj;
}

async function main(parentDir) {
  const errors = [];
  const paths = await findWals(parentDir);
  const callCounts = [];

  // Originally we moved to doing one skin at a time because we thougth it was
  // causing memory issues, now we know it's not, but it's still faster to do
  // one at a time for some reason.  ¯\_(ツ)_/¯
  for (const walPath of paths) {
    try {
      const fileName = path.basename(walPath);
      if (SKIN_FILENAME_BLACKLIST.has(fileName)) {
        continue;
      }
      console.error(`Working on ${walPath}`);
      callCounts.push(await getCallCountsFromWal(walPath));
    } catch (e) {
      const errorLine = e.toString().split("\n")[0];
      errors.push({ [errorLine]: 1 });
      // TODO: Investigate these.
      console.error(`Error getting calld data from ${walPath}`, e);
    }
  }
  const totalCalls = callCounts.reduce(sumCountObjects);
  const foundInSkins = callCounts
    .map(setObjectValuesToOne)
    .reduce(sumCountObjects);

  if (errors.length) {
    console.error(JSON.stringify(errors.reduce(sumCountObjects, {}), null, 2));
  }
  const result = { totalCalls, foundInSkins };
  console.log(JSON.stringify(result, null, 2));
}

main("/Volumes/Mobile Backup/skins/skins/");
