text: #!/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/");
  