/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

"use strict";

/**
 * Memory leak hunter. Walks a tree of objects looking for DOM nodes.
 * Usage:
 * leakHunt({
 *   thing: thing,
 *   otherthing: otherthing
 * });
 */
function leakHunt(root) {
  const path = [];
  const seen = [];

  try {
    const output = leakHunt.inner(root, path, seen);
    output.forEach(function (line) {
      dump(line + "\n");
    });
  } catch (ex) {
    dump(ex + "\n");
  }
}

leakHunt.inner = function (root, path, seen) {
  const prefix = new Array(path.length).join("  ");

  const reply = [];
  function log(msg) {
    reply.push(msg);
  }

  let direct;
  try {
    direct = Object.keys(root);
  } catch (ex) {
    log(prefix + "  Error enumerating: " + ex);
    return reply;
  }

  try {
    let index = 0;
    for (const data of root) {
      const prop = "" + index;
      leakHunt.digProperty(prop, data, path, seen, direct, log);
      index++;
    }
  } catch (ex) {
    /* Ignore things that are not enumerable */
  }

  for (const prop in root) {
    let data;
    try {
      data = root[prop];
    } catch (ex) {
      log(prefix + "  " + prop + " = Error: " + ex.toString().substring(0, 30));
      continue;
    }

    leakHunt.digProperty(prop, data, path, seen, direct, log);
  }

  return reply;
};

leakHunt.hide = [/^string$/, /^number$/, /^boolean$/, /^null/, /^undefined/];

leakHunt.noRecurse = [
  /^string$/,
  /^number$/,
  /^boolean$/,
  /^null/,
  /^undefined/,
  /^Window$/,
  /^Document$/,
  /^XULElement$/,
  /^DOMWindow$/,
  /^HTMLDocument$/,
  /^HTML.*Element$/,
  /^ChromeWindow$/,
];

leakHunt.digProperty = function (prop, data, path, seen, direct, log) {
  const newPath = path.slice();
  newPath.push(prop);
  const prefix = new Array(newPath.length).join("  ");

  let recurse = true;
  let message = leakHunt.getType(data);

  if (leakHunt.matchesAnyPattern(message, leakHunt.hide)) {
    return;
  }

  if (message === "function" && !direct.includes(prop)) {
    return;
  }

  if (message === "string") {
    const extra = data.length > 10 ? data.substring(0, 9) + "_" : data;
    message += ' "' + extra.replace(/\n/g, "|") + '"';
    recurse = false;
  } else if (leakHunt.matchesAnyPattern(message, leakHunt.noRecurse)) {
    message += " (no recurse)";
    recurse = false;
  } else if (seen.includes(data)) {
    message += " (already seen)";
    recurse = false;
  }

  if (recurse) {
    seen.push(data);
    const lines = leakHunt.inner(data, newPath, seen);
    if (!lines.length) {
      if (message !== "function") {
        log(prefix + prop + " = " + message + " { }");
      }
    } else {
      log(prefix + prop + " = " + message + " {");
      lines.forEach(function (line) {
        log(line);
      });
      log(prefix + "}");
    }
  } else {
    log(prefix + prop + " = " + message);
  }
};

leakHunt.matchesAnyPattern = function (str, patterns) {
  let match = false;
  patterns.forEach(function (pattern) {
    if (str.match(pattern)) {
      match = true;
    }
  });
  return match;
};

leakHunt.getType = function (data) {
  if (data === null) {
    return "null";
  }
  if (data === undefined) {
    return "undefined";
  }

  let type = typeof data;
  if (type === "object" || type === "Object") {
    type = leakHunt.getCtorName(data);
  }

  return type;
};

leakHunt.getCtorName = function (obj) {
  try {
    if (obj.constructor && obj.constructor.name) {
      return obj.constructor.name;
    }
  } catch (ex) {
    return "UnknownObject";
  }

  // If that fails, use Objects toString which sometimes gives something
  // better than 'Object', and at least defaults to Object if nothing better
  return Object.prototype.toString.call(obj).slice(8, -1);
};
