/* ***** 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 GCLI.
 *
 * The Initial Developer of the Original Code is
 * The Mozilla Foundation
 * Portions created by the Initial Developer are Copyright (C) 2011
 * the Initial Developer. All Rights Reserved.
 *
 * Contributor(s):
 *   Joe Walker <jwalker@mozilla.com> (Original Author)
 *
 * 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 ***** */

/*
 *
 *
 *
 *
 *
 *
 *
 *********************************** WARNING ***********************************
 *
 * Do not edit this file without understanding where it comes from,
 * Your changes are likely to be overwritten without warning.
 *
 * The original source for this file is:
 *  https://github.com/mozilla/gcli/
 *
 *******************************************************************************
 *
 *
 *
 *
 *
 *
 *
 *
 *
 */

///////////////////////////////////////////////////////////////////////////////

/*
 * This build of GCLI for Firefox is really 4 bits of code:
 * - Browser support code - Currently just an implementation of the console
 *   object that uses dump. We may need to add other browser shims to this.
 * - A very basic commonjs AMD (Asynchronous Modules Definition) 'require'
 *   implementation (which is just good enough to load GCLI). For more, see
 *   http://wiki.commonjs.org/wiki/Modules/AsynchronousDefinition.
 *   This alleviates the need for requirejs (http://requirejs.org/) which is
 *   used when running in the browser.
 *   This section of code is a copy of mini_require.js without the header and
 *   footers. Changes to one should be reflected in the other.
 * - A build of GCLI itself, packaged using dryice (for more details see the
 *   project https://github.com/mozilla/dryice and the build file in this
 *   project at Makefile.dryice.js)
 * - Lastly, code to require the gcli object as needed by EXPORTED_SYMBOLS.
 */

var EXPORTED_SYMBOLS = [ "gcli" ];


///////////////////////////////////////////////////////////////////////////////

/*
 * This creates a console object that somewhat replicates Firebug's console
 * object. It currently writes to dump(), but should write to the web
 * console's chrome error section (when it has one)
 */


/**
 * String utility to ensure that strings are a specified length. Strings
 * that are too long are truncated to the max length and the last char is
 * set to "_". Strings that are too short are left padded with spaces.
 *
 * @param {string} aStr
 *        The string to format to the correct length
 * @param {number} aMaxLen
 *        The maximum allowed length of the returned string
 * @param {number} aMinLen (optional)
 *        The minimum allowed length of the returned string. If undefined,
 *        then aMaxLen will be used
 * @param {object} aOptions (optional)
 *        An object allowing format customization. The only customization
 *        allowed currently is 'truncate' which can take the value "start" to
 *        truncate strings from the start as opposed to the end.
 * @return {string}
 *        The original string formatted to fit the specified lengths
 */
function fmt(aStr, aMaxLen, aMinLen, aOptions) {
  if (aMinLen == undefined) {
    aMinLen = aMaxLen;
  }
  if (aStr == null) {
    aStr = "";
  }
  if (aStr.length > aMaxLen) {
    if (aOptions && aOptions.truncate == "start") {
      return "_" + aStr.substring(aStr.length - aMaxLen + 1);
    }
    else {
      return aStr.substring(0, aMaxLen - 1) + "_";
    }
  }
  if (aStr.length < aMinLen) {
    return Array(aMinLen - aStr.length + 1).join(" ") + aStr;
  }
  return aStr;
}

/**
 * Utility to extract the constructor name of an object.
 * Object.toString gives: "[object ?????]"; we want the "?????".
 *
 * @param {object} aObj
 *        The object from which to extract the constructor name
 * @return {string}
 *        The constructor name
 */
function getCtorName(aObj) {
  return Object.prototype.toString.call(aObj).slice(8, -1);
}

/**
 * A single line stringification of an object designed for use by humans
 *
 * @param {any} aThing
 *        The object to be stringified
 * @return {string}
 *        A single line representation of aThing, which will generally be at
 *        most 60 chars long
 */
function stringify(aThing) {
  if (aThing === undefined) {
    return "undefined";
  }

  if (aThing === null) {
    return "null";
  }

  if (typeof aThing == "object") {
    try {
      return getCtorName(aThing) + " " + fmt(JSON.stringify(aThing), 50, 0);
    }
    catch (ex) {
      return "[stringify error]";
    }
  }

  var str = aThing.toString().replace(/\s+/g, " ");
  return fmt(str, 60, 0);
}

/**
 * A multi line stringification of an object, designed for use by humans
 *
 * @param {any} aThing
 *        The object to be stringified
 * @return {string}
 *        A multi line representation of aThing
 */
function log(aThing) {
  if (aThing == null) {
    return "null";
  }

  if (aThing == undefined) {
    return "undefined";
  }

  if (typeof aThing == "object") {
    var reply = "";
    var type = getCtorName(aThing);
    if (type == "Error") {
      reply += "  " + aThing.message + "\n";
      reply += logProperty("stack", aThing.stack);
    }
    else {
      var keys = Object.getOwnPropertyNames(aThing);
      if (keys.length > 0) {
        reply += type + "\n";
        keys.forEach(function(aProp) {
          reply += logProperty(aProp, aThing[aProp]);
        }, this);
      }
      else {
        reply += type + " (enumerated with for-in)\n";
        var prop;
        for (prop in aThing) {
          reply += logProperty(prop, aThing[prop]);
        }
      }
    }

    return reply;
  }

  return "  " + aThing.toString() + "\n";
}

/**
 * Helper for log() which converts a property/value pair into an output
 * string
 *
 * @param {string} aProp
 *        The name of the property to include in the output string
 * @param {object} aValue
 *        Value assigned to aProp to be converted to a single line string
 * @return {string}
 *        Multi line output string describing the property/value pair
 */
function logProperty(aProp, aValue) {
  var reply = "";
  if (aProp == "stack" && typeof value == "string") {
    var trace = parseStack(aValue);
    reply += formatTrace(trace);
  }
  else {
    reply += "    - " + aProp + " = " + stringify(aValue) + "\n";
  }
  return reply;
}

/**
 * Parse a stack trace, returning an array of stack frame objects, where
 * each has file/line/call members
 *
 * @param {string} aStack
 *        The serialized stack trace
 * @return {object[]}
 *        Array of { file: "...", line: NNN, call: "..." } objects
 */
function parseStack(aStack) {
  var trace = [];
  aStack.split("\n").forEach(function(line) {
    if (!line) {
      return;
    }
    var at = line.lastIndexOf("@");
    var posn = line.substring(at + 1);
    trace.push({
      file: posn.split(":")[0],
      line: posn.split(":")[1],
      call: line.substring(0, at)
    });
  }, this);
  return trace;
}

/**
 * parseStack() takes output from an exception from which it creates the an
 * array of stack frame objects, this has the same output but using data from
 * Components.stack
 *
 * @param {string} aFrame
 *        The stack frame from which to begin the walk
 * @return {object[]}
 *        Array of { file: "...", line: NNN, call: "..." } objects
 */
function getStack(aFrame) {
  if (!aFrame) {
    aFrame = Components.stack.caller;
  }
  var trace = [];
  while (aFrame) {
    trace.push({
      file: aFrame.filename,
      line: aFrame.lineNumber,
      call: aFrame.name
    });
    aFrame = aFrame.caller;
  }
  return trace;
};

/**
 * Take the output from parseStack() and convert it to nice readable
 * output
 *
 * @param {object[]} aTrace
 *        Array of trace objects as created by parseStack()
 * @return {string} Multi line report of the stack trace
 */
function formatTrace(aTrace) {
  var reply = "";
  aTrace.forEach(function(frame) {
    reply += fmt(frame.file, 20, 20, { truncate: "start" }) + " " +
             fmt(frame.line, 5, 5) + " " +
             fmt(frame.call, 75, 75) + "\n";
  });
  return reply;
}

/**
 * Create a function which will output a concise level of output when used
 * as a logging function
 *
 * @param {string} aLevel
 *        A prefix to all output generated from this function detailing the
 *        level at which output occurred
 * @return {function}
 *        A logging function
 * @see createMultiLineDumper()
 */
function createDumper(aLevel) {
  return function() {
    var args = Array.prototype.slice.call(arguments, 0);
    var data = args.map(function(arg) {
      return stringify(arg);
    });
    dump(aLevel + ": " + data.join(", ") + "\n");
  };
}

/**
 * Create a function which will output more detailed level of output when
 * used as a logging function
 *
 * @param {string} aLevel
 *        A prefix to all output generated from this function detailing the
 *        level at which output occurred
 * @return {function}
 *        A logging function
 * @see createDumper()
 */
function createMultiLineDumper(aLevel) {
  return function() {
    dump(aLevel + "\n");
    var args = Array.prototype.slice.call(arguments, 0);
    args.forEach(function(arg) {
      dump(log(arg));
    });
  };
}

/**
 * The console object to expose
 */
var console = {
  debug: createMultiLineDumper("debug"),
  log: createDumper("log"),
  info: createDumper("info"),
  warn: createDumper("warn"),
  error: createMultiLineDumper("error"),
  trace: function Console_trace() {
    var trace = getStack(Components.stack.caller);
    dump(formatTrace(trace) + "\n");
  },
  clear: function Console_clear() {},

  dir: createMultiLineDumper("dir"),
  dirxml: createMultiLineDumper("dirxml"),
  group: createDumper("group"),
  groupEnd: createDumper("groupEnd")
};


///////////////////////////////////////////////////////////////////////////////

// There are 2 virtually identical copies of this code:
// - $GCLI_HOME/build/prefix-gcli.jsm
// - $GCLI_HOME/build/mini_require.js
// They should both be kept in sync

var debugDependencies = false;

/**
 * Define a module along with a payload.
 * @param {string} moduleName Name for the payload
 * @param {ignored} deps Ignored. For compatibility with CommonJS AMD Spec
 * @param {function} payload Function with (require, exports, module) params
 */
function define(moduleName, deps, payload) {
  if (typeof moduleName != "string") {
    console.error(this.depth + " Error: Module name is not a string.");
    console.trace();
    return;
  }

  if (arguments.length == 2) {
    payload = deps;
  }

  if (debugDependencies) {
    console.log("define: " + moduleName + " -> " + payload.toString()
        .slice(0, 40).replace(/\n/, '\\n').replace(/\r/, '\\r') + "...");
  }

  if (moduleName in define.modules) {
    console.error(this.depth + " Error: Redefining module: " + moduleName);
  }
  define.modules[moduleName] = payload;
};

/**
 * The global store of un-instantiated modules
 */
define.modules = {};


/**
 * We invoke require() in the context of a Domain so we can have multiple
 * sets of modules running separate from each other.
 * This contrasts with JSMs which are singletons, Domains allows us to
 * optionally load a CommonJS module twice with separate data each time.
 * Perhaps you want 2 command lines with a different set of commands in each,
 * for example.
 */
function Domain() {
  this.modules = {};

  if (debugDependencies) {
    this.depth = "";
  }
}

/**
 * Lookup module names and resolve them by calling the definition function if
 * needed.
 * There are 2 ways to call this, either with an array of dependencies and a
 * callback to call when the dependencies are found (which can happen
 * asynchronously in an in-page context) or with a single string an no callback
 * where the dependency is resolved synchronously and returned.
 * The API is designed to be compatible with the CommonJS AMD spec and
 * RequireJS.
 * @param {string[]|string} deps A name, or names for the payload
 * @param {function|undefined} callback Function to call when the dependencies
 * are resolved
 * @return {undefined|object} The module required or undefined for
 * array/callback method
 */
Domain.prototype.require = function(deps, callback) {
  if (Array.isArray(deps)) {
    var params = deps.map(function(dep) {
      return this.lookup(dep);
    }, this);
    if (callback) {
      callback.apply(null, params);
    }
    return undefined;
  }
  else {
    return this.lookup(deps);
  }
};

/**
 * Lookup module names and resolve them by calling the definition function if
 * needed.
 * @param {string} moduleName A name for the payload to lookup
 * @return {object} The module specified by aModuleName or null if not found.
 */
Domain.prototype.lookup = function(moduleName) {
  if (moduleName in this.modules) {
    var module = this.modules[moduleName];
    if (debugDependencies) {
      console.log(this.depth + " Using module: " + moduleName);
    }
    return module;
  }

  if (!(moduleName in define.modules)) {
    console.error(this.depth + " Missing module: " + moduleName);
    return null;
  }

  var module = define.modules[moduleName];

  if (debugDependencies) {
    console.log(this.depth + " Compiling module: " + moduleName);
  }

  if (typeof module == "function") {
    if (debugDependencies) {
      this.depth += ".";
    }

    var exports = {};
    try {
      module(this.require.bind(this), exports, { id: moduleName, uri: "" });
    }
    catch (ex) {
      console.error("Error using module: " + moduleName, ex);
      throw ex;
    }
    module = exports;

    if (debugDependencies) {
      this.depth = this.depth.slice(0, -1);
    }
  }

  // cache the resulting module object for next time
  this.modules[moduleName] = module;

  return module;
};

/**
 * Expose the Domain constructor and a global domain (on the define function
 * to avoid exporting more than we need. This is a common pattern with require
 * systems)
 */
define.Domain = Domain;
define.globalDomain = new Domain();

/**
 * Expose a default require function which is the require of the global
 * sandbox to make it easy to use.
 */
var require = define.globalDomain.require.bind(define.globalDomain);


///////////////////////////////////////////////////////////////////////////////

/*
 * The API of interest to people wanting to create GCLI commands is as
 * follows. The implementation of this API is left to bug 659061 and other
 * bugs.
 */

define('gcli/index', [ ], function(require, exports, module) {

  exports.addCommand = function() { /* implementation goes here */ };
  exports.removeCommand = function() { /* implementation goes here */ };
  exports.startup = function() { /* implementation goes here */ };
  exports.shutdown = function() { /* implementation goes here */ };

});

///////////////////////////////////////////////////////////////////////////////

/*
 * require GCLI so it can be exported as declared in EXPORTED_SYMBOLS
 * The dependencies specified here should be the same as in Makefile.dryice.js
 */
var gcli = require("gcli/index");
gcli.createView = require("gcli/ui/start/firefox");
gcli._internal = { require: require, define: define, console: console };

