/* ***** BEGIN LICENSE BLOCK *****
 * Version: MPL 1.1/GPL 2.0/LGPL 2.1
 *
 * The contents of this file are subject to the Mozilla Public License Version
 * 1.1 (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 * http://www.mozilla.org/MPL/
 *
 * Software distributed under the License is distributed on an "AS IS" basis,
 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
 * for the specific language governing rights and limitations under the
 * License.
 *
 * The Original Code is ECMAScript debugging functions.
 *
 * The Initial Developer of the Original Code is
 * Alexander J. Vincent <ajvincent@gmail.com>.
 * Portions created by the Initial Developer are Copyright (C) 2004-2007
 * the Initial Developer. All Rights Reserved.
 *
 * Contributor(s):
 *
 * Alternatively, the contents of this file may be used under the terms of
 * either the GNU General Public License Version 2 or later (the "GPL"), or
 * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
 * in which case the provisions of the GPL or the LGPL are applicable instead
 * of those above. If you wish to allow use of your version of this file only
 * under the terms of either the GPL or the LGPL, and not to allow others to
 * use your version of this file under the terms of the MPL, indicate your
 * decision by deleting the provisions above and replace them with the notice
 * and other provisions required by the GPL or the LGPL. If you do not delete
 * the provisions above, a recipient may use your version of this file under
 * the terms of any one of the MPL, the GPL or the LGPL.
 *
 * ***** END LICENSE BLOCK ***** */

var gPrefService = null;
var gECMADebugPrefBranch = null;
var gDebugPrefSet = false;
var gDebugPref = false;

const nsIDebug          = Components.interfaces.nsIDebug;
const nsIScriptError    = Components.interfaces.nsIScriptError;
const nsIConsoleService = Components.interfaces.nsIConsoleService;

const consoleService = Components.classes["@mozilla.org/consoleservice;1"]
                                 .getService(nsIConsoleService);

const nsIDebugService = Components.classes["@mozilla.org/xpcom/debug;1"]
                                  .getService(nsIDebug);

function getDebugPref() {
  if (gDebugPrefSet) {
    return gDebugPref;
  }

  if (!gPrefService) {
    const nsIPrefService = Components.interfaces.nsIPrefService;
    gPrefService = Components.classes["@mozilla.org/preferences-service;1"]
                             .getService(nsIPrefService);
  }

  if (!gECMADebugPrefBranch) {
    const nsIPrefBranch2 = Components.interfaces.nsIPrefBranch2;
    gECMADebugPrefBranch = gPrefService.getBranch(null)
                                       .QueryInterface(nsIPrefBranch2);
  }

  if (gECMADebugPrefBranch.getPrefType("javascript.debug")
      != gECMADebugPrefBranch.PREF_BOOL) {
    assertWithStack(); // set the preference by default, you moron!
    gDebugPref = false;
  } else {
    gDebugPref = gECMADebugPrefBranch.getBoolPref("javascript.debug");
  }
  gDebugPrefSet = true;
  return gDebugPref;
}

function NS_ASSERTION(mustBeTrue, errMsg) {
  // Use this ONLY when you want to crash the program!
  if (mustBeTrue) {
    return;
  }
  dumpJSStack();

  var realFrame = Components.stack.caller;
  nsIDebugService.assertion("Script reports: " + errMsg,
                            mustBeTrue,
                            realFrame.filename,
                            realFrame.lineNumber
                           );
}

function dispatchMessage(aError, aMessageFlag) {
  var aMessage = Components.classes["@mozilla.org/scripterror;1"]
                           .createInstance(nsIScriptError);
  aMessage.init(aError.message,
                aError.fileName,
                "",
                aError.lineNumber,
                0,
                aMessageFlag,
                "");

  consoleService.logMessage(aMessage);
}

function warn(aIncomingError) {
  var ecmaDebug = getDebugPref();
  if (!ecmaDebug)
    return;
  var err;
  if (aIncomingError instanceof Error) {
    err = aIncomingError;
  } else {
    // We want the error initialized, including stack.
    err = new Error(aIncomingError.toString());
    // Get to the actual warning.
    err.shiftStack(2); 
  }
  dispatchMessage(err, nsIScriptError.warningFlag);
}

function assert(mustBeTrue, errMsg) {
  var ecmaDebug = getDebugPref();
  if (!ecmaDebug || mustBeTrue) {
    return mustBeTrue;
  }

  var e = new Error(errMsg);
  // This takes us to the actual stack error.
  e.shiftStack(2);

  e.name = "AssertionError";
  // Reset the error message.
  e.message = errMsg;
  dump(e.stack + "\n\n");
  throw e;
}

Error.prototype.shiftStack = function(aShiftLines) {
  var stackArray  = this.stack.split("\n").splice(aShiftLines);
  var stackTop    = stackArray[0];
  var lastColon   = stackTop.lastIndexOf(":");
  this.fileName   = stackTop.substring(stackTop.indexOf("@") + 1, lastColon);
  this.lineNumber = stackTop.substr(lastColon + 1);
  this.stack      = stackArray.join("\n");
}

/**
 * Return a function to execute a ECMA debug contract.
 *
 * @param aContract Object with body, optional pre- and post-condition methods.
 */
function getContractFunction(aContract) {
  if (typeof aContract.body != "function")
    throw new Error("Invalid contract passed into getContractFunction!");

  if (typeof aContract.precondition != "function")
    aContract.precondition = null;

  if (typeof aContract.postcondition != "function")
    aContract.postcondition = null;

  var ecmaDebug = getDebugPref();

  function executeContract() {
    if (ecmaDebug && aContract.precondition) {
      aContract.precondition.apply(this, arguments);
    }

    var rv = aContract.body.apply(this, arguments);

    if (ecmaDebug && aContract.postcondition) {
      var args = [];
      for (var x = 0; x < arguments.length; x++) {
        args[x] = arguments[x];
      }

      args.push(rv);
      aContract.postcondition.apply(this, args);
    }

    return rv;
  }

  function executeContractAsConstructor() {
    if (ecmaDebug && aContract.precondition) {
      aContract.precondition.apply(this, arguments);
    }

    this.__proto__ = aContract.prototype;

    aContract.body.apply(this, arguments);

    if (ecmaDebug && aContract.postcondition) {
      aContract.postcondition.apply(this, arguments);
    }
  }

  var rv;
  if (typeof aContract.isConstructor == "boolean" && aContract.isConstructor) 
    rv = executeContractAsConstructor;
  else
    rv = executeContract;

  rv.isContractFunction = true;
  rv.precondition = aContract.precondition;
  rv.postcondition = aContract.postcondition;
  rv.body = aContract.body;

  rv.toContractString = function toContractString() {
    return aContract.toString();
  }

  rv.toContractSource = function toContractSource() {
    return aContract.toSource();
  }

  rv.toString = function toString() {
    return aContract.body.toString();
  }

  rv.toSource = function toSource() {
    return aContract.body.toSource();
  }

  return rv;
}

/**
 * Construct an object to enforce invariants on a JS-based XPCOM component.
 *
 * @param aComponent XPCOM component to wrap in invariant checking.
 */
function InvariantContract(aComponent) {
  var ecmaDebug = getDebugPref();
  if (!ecmaDebug) {
    throw new Error("Don't construct an InvariantContract with ecmaDebug off!");
  }

  const nsIClassInfo = Components.interfaces.nsIClassInfo;
  assert(aComponent.QueryInterface(nsIClassInfo), "No class info?");
  try {
    assert(typeof aComponent.invariant == "function", "No invariant function?");
  } catch (e) {
    dump(aComponent + "\n\n");
    dump(e.stack + "\n\n");
    throw e;
  }
  this._baseComponent = aComponent;
  
  var aCount = {};
  var allInterfaces = aComponent.getInterfaces(aCount);
  allInterfaces.forEach(this._processInterface, this);
  
  // Check the invariant now.
  aComponent.invariant();
}
InvariantContract.prototype = {
  _generateContract: function _generateContract(aName, aFunc) {
    const base = this._baseComponent;
    var contract = {
      precondition: function invariant_precondition() {
        if (contract.method_precondition) {
          contract.method_precondition.apply(this, arguments);
        }
      },

      method_precondition: null,
      body: aFunc,
      method_postcondition: null,

      postcondition: function invariant_postcondition() {
        if (contract.method_postcondition) {
          contract.method_postcondition.apply(this, arguments);
        }
        base.invariant();
      }
    }
    
    if ((typeof aFunc.isContractFunction == "boolean") &&
        (aFunc.isContractFunction == true)) {
      // If we already have a contract method, reuse it.
      contract.method_precondition = aFunc.precondition;
      contract.method_postcondition = aFunc.postcondition;
      contract.body = aFunc.body;
      try {
        assert(typeof contract.body == "function", "No contract body?");
      } catch (e) {
        dump(e.stack + "\n\n");
        throw e;
      }
    } else {

      if (typeof base[aName + "_pre"] == "function") {
        contract.method_precondition = base[aName + "_pre"];
      }
  
      if (typeof base[aName + "_post"] == "function") {
        contract.method_postcondition = base[aName + "_post"];
      }
    }
    return getContractFunction(contract);
  },

  _createGetter: function _createGetter(aName) {
    function get_value_body() {
      return this._baseComponent[aName];
    }

    var contractFunction = this._generateContract(aName, get_value_body);
    return contractFunction;
  },

  _createSetter: function _createSetter(aName) {
    function set_value_body(val) {
      var retval = this._baseComponent[aName] = val;
      return retval;
    }

    var contractFunction = this._generateContract(aName, set_value_body);
    return contractFunction;
  },
  
  _createMethod: function _createMethod(aName) {
    const base = this._baseComponent;
    // Enforce our component contract.
    var contract = this._generateContract(aName, base[aName]);

    // Now apply the invariant of the whole component.
    return function aNewMethod() {
      return contract.apply(base, arguments);
    }
  },

  _processInterface: function _processInterface(aInterface) {
    // inspired by mozilla.org's nsInterfaceInfoToIDL.js@doInterface()
    const infoClassID = "@mozilla.org/scriptableInterfaceInfo;1";
    const IInfo = new Components.Constructor(infoClassID,
                                             "nsIScriptableInterfaceInfo",
                                             "init");
    var info = new IInfo(aInterface);
    var parent = info.parent;

    // Iterate through this interface's methods and attributes.
    var methodBaseIndex = parent ? parent.methodCount : 0;
    var methodTotalCount =  info.methodCount;
    var methodLocalCount = methodTotalCount - methodBaseIndex;
    for (i = methodBaseIndex; i < methodTotalCount; i++) {
      var m = info.getMethodInfo(i);
      if (m.isNotXPCOM ||
          m.isHidden /* [noscript] */ ) {
        continue;
      }

      if (m.isGetter) {
        // attribute getter
        this.__defineGetter__(m.name, this._createGetter(m.name));
        // figure out if this is readonly
        var m2 = i + 1 < methodTotalCount ? info.getMethodInfo(i + 1) : null;
        var readonly = !m2 || m2.name != m.name;
        if (!readonly) {
          this.__defineSetter__(m2.name, this._createSetter(m.name));
          i++;
        }

        continue;
      }

      // It's not a getter or a setter, so it's a real method.
      this[m.name] = this._createMethod(m.name);
    }
  },

  // nsISupports
  QueryInterface: function QueryInterface(aIID) {
    return (this._baseComponent.QueryInterface(aIID)) ? this : null;
  },
  
  toString: function toString() {
    return "[InvariantContract " + this._baseComponent.toString() + "]";
  }
}

function dumpJSStack() {
  var err = new Error("foo");
  err.shiftStack(2);
  dump(err.stack);
}

const EXPORTED_SYMBOLS = ["getDebugPref",
                          "NS_ASSERTION",
                          "dispatchMessage",
                          "warn",
                          "assert",
                          "getContractFunction",
                          "InvariantContract",
                          "dumpJSStack"];
