/* Any copyright is dedicated to the Public Domain.
   http://creativecommons.org/publicdomain/zero/1.0/ */

"use strict";

var { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
var { Services } = Cu.import("resource://gre/modules/Services.jsm", {});
var { gDevTools } = Cu.import("resource://devtools/client/framework/gDevTools.jsm", {});
var { console } = Cu.import("resource://gre/modules/Console.jsm", {});
var { require } = Cu.import("resource://devtools/shared/Loader.jsm", {});

var DevToolsUtils = require("devtools/shared/DevToolsUtils");
DevToolsUtils.testing = true;

var { OS } = require("resource://gre/modules/osfile.jsm");
var { FileUtils } = require("resource://gre/modules/FileUtils.jsm");
var { TargetFactory } = require("devtools/client/framework/target");
var promise = require("promise");
var { Task } = Cu.import("resource://gre/modules/Task.jsm", {});
var { expectState } = require("devtools/server/actors/common");
var HeapSnapshotFileUtils = require("devtools/shared/heapsnapshot/HeapSnapshotFileUtils");
var HeapAnalysesClient = require("devtools/shared/heapsnapshot/HeapAnalysesClient");
var { addDebuggerToGlobal } = require("resource://gre/modules/jsdebugger.jsm");
var Store = require("devtools/client/memory/store");
var SYSTEM_PRINCIPAL = Cc["@mozilla.org/systemprincipal;1"].createInstance(Ci.nsIPrincipal);

function dumpn(msg) {
  dump(`MEMORY-TEST: ${msg}\n`);
}

function initDebugger () {
  let global = new Cu.Sandbox(SYSTEM_PRINCIPAL, { freshZone: true });
  addDebuggerToGlobal(global);
  return new global.Debugger();
}

function StubbedMemoryFront () {
  this.state = "detached";
  this.recordingAllocations = false;
  this.dbg = initDebugger();
}

StubbedMemoryFront.prototype.attach = Task.async(function *() {
  this.state = "attached";
});

StubbedMemoryFront.prototype.detach = Task.async(function *() {
  this.state = "detached";
});

StubbedMemoryFront.prototype.saveHeapSnapshot = expectState("attached", Task.async(function *() {
  return ThreadSafeChromeUtils.saveHeapSnapshot({ runtime: true });
}), "saveHeapSnapshot");

StubbedMemoryFront.prototype.startRecordingAllocations = expectState("attached", Task.async(function* () {
  this.recordingAllocations = true;
}));

StubbedMemoryFront.prototype.stopRecordingAllocations = expectState("attached", Task.async(function* () {
  this.recordingAllocations = false;
}));

function waitUntilState (store, predicate) {
  let deferred = promise.defer();
  let unsubscribe = store.subscribe(check);

  function check () {
    if (predicate(store.getState())) {
      unsubscribe();
      deferred.resolve()
    }
  }

  // Fire the check immediately incase the action has already occurred
  check();

  return deferred.promise;
}

function waitUntilAction (store, actionType) {
  let deferred = promise.defer();
  let unsubscribe = store.subscribe(check);
  let history = store.history;
  let index = history.length;

  do_print(`Waiting for action "${actionType}"`);
  function check () {
    let action = history[index++];
    if (action && action.type === actionType) {
      do_print(`Found action "${actionType}"`);
      unsubscribe();
      deferred.resolve(store.getState());
    }
  }

  return deferred.promise;
}

function waitUntilSnapshotState (store, expected) {
  let predicate = () => {
    let snapshots = store.getState().snapshots;
    do_print(snapshots.map(x => x.state));
    return snapshots.length === expected.length &&
           expected.every((state, i) => state === "*" || snapshots[i].state === state);
  };
  do_print(`Waiting for snapshots to be of state: ${expected}`);
  return waitUntilState(store, predicate);
}

function isBreakdownType (report, type) {
  // Little sanity check, all reports should have at least a children array.
  if (!report || !Array.isArray(report.children)) {
    return false;
  }
  switch (type) {
    case "coarseType":
      return report.children.find(c => c.name === "objects");
    case "objectClass":
      return report.children.find(c => c.name === "Function");
    case "internalType":
      return report.children.find(c => c.name === "js::BaseShape") &&
             !report.children.find(c => c.name === "objects");
    default:
      throw new Error(`isBreakdownType does not yet support ${type}`);
  }
}

function *createTempFile () {
  let file = FileUtils.getFile("TmpD", ["tmp.fxsnapshot"]);
  file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE);
  let destPath = file.path;
  let stat = yield OS.File.stat(destPath);
  ok(stat.size === 0, "new file is 0 bytes at start");
  return destPath;
}
