/* 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";

const {Cc, Ci, Cu, Cr} = require("chrome");
const events = require("sdk/event/core");
const protocol = require("devtools/server/protocol");
const {serializeStack, parseStack} = require("toolkit/loader");

const {on, once, off, emit} = events;
const {method, Arg, Option, RetVal} = protocol;

/**
 * Type describing a single function call in a stack trace.
 */
protocol.types.addDictType("call-stack-item", {
  name: "string",
  file: "string",
  line: "number"
});

/**
 * Type describing an overview of a function call.
 */
protocol.types.addDictType("call-details", {
  type: "number",
  name: "string",
  stack: "array:call-stack-item"
});

/**
 * This actor contains information about a function call, like the function
 * type, name, stack, arguments, returned value etc.
 */
var FunctionCallActor = protocol.ActorClass({
  typeName: "function-call",

  /**
   * Creates the function call actor.
   *
   * @param DebuggerServerConnection conn
   *        The server connection.
   * @param DOMWindow window
   *        The content window.
   * @param string global
   *        The name of the global object owning this function, like
   *        "CanvasRenderingContext2D" or "WebGLRenderingContext".
   * @param object caller
   *        The object owning the function when it was called.
   *        For example, in `foo.bar()`, the caller is `foo`.
   * @param number type
   *        Either METHOD_FUNCTION, METHOD_GETTER or METHOD_SETTER.
   * @param string name
   *        The called function's name.
   * @param array stack
   *        The called function's stack, as a list of { name, file, line } objects.
   * @param number timestamp
   *        The timestamp of draw-related functions
   * @param array args
   *        The called function's arguments.
   * @param any result
   *        The value returned by the function call.
   * @param boolean holdWeak
   *        Determines whether or not FunctionCallActor stores a weak reference
   *        to the underlying objects.
   */
  initialize: function(conn, [window, global, caller, type, name, stack, timestamp, args, result], holdWeak) {
    protocol.Actor.prototype.initialize.call(this, conn);

    this.details = {
      type: type,
      name: name,
      stack: stack,
      timestamp: timestamp
    };

    // Store a weak reference to all objects so we don't
    // prevent natural GC if `holdWeak` was passed into
    // setup as truthy. Used in the Web Audio Editor.
    if (holdWeak) {
      let weakRefs = {
        window: Cu.getWeakReference(window),
        caller: Cu.getWeakReference(caller),
        result: Cu.getWeakReference(result),
        args: Cu.getWeakReference(args)
      };

      Object.defineProperties(this.details, {
        window: { get: () => weakRefs.window.get() },
        caller: { get: () => weakRefs.caller.get() },
        result: { get: () => weakRefs.result.get() },
        args: { get: () => weakRefs.args.get() },
        timestamp: { get: () => weakRefs.timestamp.get() },
      });
    }
    // Otherwise, hold strong references to the objects.
    else {
      this.details.window = window;
      this.details.caller = caller;
      this.details.result = result;
      this.details.args = args;
      this.details.timestamp = timestamp;
    }

    this.meta = {
      global: -1,
      previews: { caller: "", args: "" }
    };

    if (global == "WebGLRenderingContext") {
      this.meta.global = CallWatcherFront.CANVAS_WEBGL_CONTEXT;
    } else if (global == "CanvasRenderingContext2D") {
      this.meta.global = CallWatcherFront.CANVAS_2D_CONTEXT;
    } else if (global == "window") {
      this.meta.global = CallWatcherFront.UNKNOWN_SCOPE;
    } else {
      this.meta.global = CallWatcherFront.GLOBAL_SCOPE;
    }

    this.meta.previews.caller = this._generateCallerPreview();
    this.meta.previews.args = this._generateArgsPreview();
  },

  /**
   * Customize the marshalling of this actor to provide some generic information
   * directly on the Front instance.
   */
  form: function() {
    return {
      actor: this.actorID,
      type: this.details.type,
      name: this.details.name,
      file: this.details.stack[0].file,
      line: this.details.stack[0].line,
      timestamp: this.details.timestamp,
      callerPreview: this.meta.previews.caller,
      argsPreview: this.meta.previews.args
    };
  },

  /**
   * Gets more information about this function call, which is not necessarily
   * available on the Front instance.
   */
  getDetails: method(function() {
    let { type, name, stack, timestamp } = this.details;

    // Since not all calls on the stack have corresponding owner files (e.g.
    // callbacks of a requestAnimationFrame etc.), there's no benefit in
    // returning them, as the user can't jump to the Debugger from them.
    for (let i = stack.length - 1;;) {
      if (stack[i].file) {
        break;
      }
      stack.pop();
      i--;
    }

    // XXX: Use grips for objects and serialize them properly, in order
    // to add the function's caller, arguments and return value. Bug 978957.
    return {
      type: type,
      name: name,
      stack: stack,
      timestamp: timestamp
    };
  }, {
    response: { info: RetVal("call-details") }
  }),

  /**
   * Serializes the caller's name so that it can be easily be transferred
   * as a string, but still be useful when displayed in a potential UI.
   *
   * @return string
   *         The caller's name as a string.
   */
  _generateCallerPreview: function() {
    let global = this.meta.global;
    if (global == CallWatcherFront.CANVAS_WEBGL_CONTEXT) {
      return "gl";
    }
    if (global == CallWatcherFront.CANVAS_2D_CONTEXT) {
      return "ctx";
    }
    return "";
  },

  /**
   * Serializes the arguments so that they can be easily be transferred
   * as a string, but still be useful when displayed in a potential UI.
   *
   * @return string
   *         The arguments as a string.
   */
  _generateArgsPreview: function() {
    let { caller, args, name } = this.details;
    let { global } = this.meta;

    // Get method signature to determine if there are any enums
    // used in this method.
    let enumArgs = (CallWatcherFront.ENUM_METHODS[global] || {})[name];
    if (typeof enumArgs === "function") {
      enumArgs = enumArgs(args);
    }

    // XXX: All of this sucks. Make this smarter, so that the frontend
    // can inspect each argument, be it object or primitive. Bug 978960.
    let serializeArgs = () => args.map((arg, i) => {
      if (arg === undefined) {
        return "undefined";
      }
      if (arg === null) {
        return "null";
      }
      if (typeof arg == "function") {
        return "Function";
      }
      if (typeof arg == "object") {
        return "Object";
      }
      // If this argument matches the method's signature
      // and is an enum, change it to its constant name.
      if (enumArgs && enumArgs.indexOf(i) !== -1) {
        return getBitToEnumValue(global, caller, arg);
      }
      return arg;
    });

    return serializeArgs().join(", ");
  }
});

/**
 * The corresponding Front object for the FunctionCallActor.
 */
var FunctionCallFront = protocol.FrontClass(FunctionCallActor, {
  initialize: function(client, form) {
    protocol.Front.prototype.initialize.call(this, client, form);
  },

  /**
   * Adds some generic information directly to this instance,
   * to avoid extra roundtrips.
   */
  form: function(form) {
    this.actorID = form.actor;
    this.type = form.type;
    this.name = form.name;
    this.file = form.file;
    this.line = form.line;
    this.timestamp = form.timestamp;
    this.callerPreview = form.callerPreview;
    this.argsPreview = form.argsPreview;
  }
});

/**
 * This actor observes function calls on certain objects or globals.
 */
var CallWatcherActor = exports.CallWatcherActor = protocol.ActorClass({
  typeName: "call-watcher",
  initialize: function(conn, tabActor) {
    protocol.Actor.prototype.initialize.call(this, conn);
    this.tabActor = tabActor;
    this._onGlobalCreated = this._onGlobalCreated.bind(this);
    this._onGlobalDestroyed = this._onGlobalDestroyed.bind(this);
    this._onContentFunctionCall = this._onContentFunctionCall.bind(this);
  },
  destroy: function(conn) {
    protocol.Actor.prototype.destroy.call(this, conn);
    this.finalize();
  },

  /**
   * Starts waiting for the current tab actor's document global to be
   * created, in order to instrument the specified objects and become
   * aware of everything the content does with them.
   */
  setup: method(function({ tracedGlobals, tracedFunctions, startRecording, performReload, holdWeak, storeCalls }) {
    if (this._initialized) {
      return;
    }
    this._initialized = true;

    this._functionCalls = [];
    this._tracedGlobals = tracedGlobals || [];
    this._tracedFunctions = tracedFunctions || [];
    this._holdWeak = !!holdWeak;
    this._storeCalls = !!storeCalls;

    on(this.tabActor, "window-ready", this._onGlobalCreated);
    on(this.tabActor, "window-destroyed", this._onGlobalDestroyed);

    if (startRecording) {
      this.resumeRecording();
    }
    if (performReload) {
      this.tabActor.window.location.reload();
    }
  }, {
    request: {
      tracedGlobals: Option(0, "nullable:array:string"),
      tracedFunctions: Option(0, "nullable:array:string"),
      startRecording: Option(0, "boolean"),
      performReload: Option(0, "boolean"),
      holdWeak: Option(0, "boolean"),
      storeCalls: Option(0, "boolean")
    },
    oneway: true
  }),

  /**
   * Stops listening for document global changes and puts this actor
   * to hibernation. This method is called automatically just before the
   * actor is destroyed.
   */
  finalize: method(function() {
    if (!this._initialized) {
      return;
    }
    this._initialized = false;
    this._finalized = true;

    off(this.tabActor, "window-ready", this._onGlobalCreated);
    off(this.tabActor, "window-destroyed", this._onGlobalDestroyed);

    this._tracedGlobals = null;
    this._tracedFunctions = null;
  }, {
    oneway: true
  }),

  /**
   * Returns whether the instrumented function calls are currently recorded.
   */
  isRecording: method(function() {
    return this._recording;
  }, {
    response: RetVal("boolean")
  }),

  /**
   * Initialize frame start timestamp for measuring
   */
  initFrameStartTimestamp: method(function() {
    this._frameStartTimestamp = this.tabActor.window.performance.now();
  }),

  /**
   * Starts recording function calls.
   */
  resumeRecording: method(function() {
    this._recording = true;
  }),

  /**
   * Stops recording function calls.
   */
  pauseRecording: method(function() {
    this._recording = false;
    return this._functionCalls;
  }, {
    response: { calls: RetVal("array:function-call") }
  }),

  /**
   * Erases all the recorded function calls.
   * Calling `resumeRecording` or `pauseRecording` does not erase history.
   */
  eraseRecording: method(function() {
    this._functionCalls = [];
  }),

  /**
   * Lightweight listener invoked whenever an instrumented function is called
   * while recording. We're doing this to avoid the event emitter overhead,
   * since this is expected to be a very hot function.
   */
  onCall: function() {},

  /**
   * Invoked whenever the current tab actor's document global is created.
   */
  _onGlobalCreated: function({window, id, isTopLevel}) {
    let self = this;

    // TODO: bug 981748, support more than just the top-level documents.
    if (!isTopLevel) {
      return;
    }
    this._tracedWindowId = id;

    let unwrappedWindow = XPCNativeWrapper.unwrap(window);
    let callback = this._onContentFunctionCall;

    for (let global of this._tracedGlobals) {
      let prototype = unwrappedWindow[global].prototype;
      let properties = Object.keys(prototype);
      properties.forEach(name => overrideSymbol(global, prototype, name, callback));
    }

    for (let name of this._tracedFunctions) {
      overrideSymbol("window", unwrappedWindow, name, callback);
    }

    /**
     * Instruments a method, getter or setter on the specified target object to
     * invoke a callback whenever it is called.
     */
    function overrideSymbol(global, target, name, callback) {
      let propertyDescriptor = Object.getOwnPropertyDescriptor(target, name);

      if (propertyDescriptor.get || propertyDescriptor.set) {
        overrideAccessor(global, target, name, propertyDescriptor, callback);
        return;
      }
      if (propertyDescriptor.writable && typeof propertyDescriptor.value == "function") {
        overrideFunction(global, target, name, propertyDescriptor, callback);
        return;
      }
    }

    /**
     * Instruments a function on the specified target object.
     */
    function overrideFunction(global, target, name, descriptor, callback) {
      // Invoking .apply on an unxrayed content function doesn't work, because
      // the arguments array is inaccessible to it. Get Xrays back.
      let originalFunc = Cu.unwaiveXrays(target[name]);

      Cu.exportFunction(function(...args) {
        let result;
        try {
          result = Cu.waiveXrays(originalFunc.apply(this, args));
        } catch (e) {
          throw createContentError(e, unwrappedWindow);
        }

        if (self._recording) {
          let timestamp = self.tabActor.window.performance.now() - self._frameStartTimestamp;
          let stack = getStack(name);
          let type = CallWatcherFront.METHOD_FUNCTION;
          callback(unwrappedWindow, global, this, type, name, stack, timestamp, args, result);
        }
        return result;
      }, target, { defineAs: name });

      Object.defineProperty(target, name, {
        configurable: descriptor.configurable,
        enumerable: descriptor.enumerable,
        writable: true
      });
    }

    /**
     * Instruments a getter or setter on the specified target object.
     */
    function overrideAccessor(global, target, name, descriptor, callback) {
      // Invoking .apply on an unxrayed content function doesn't work, because
      // the arguments array is inaccessible to it. Get Xrays back.
      let originalGetter = Cu.unwaiveXrays(target.__lookupGetter__(name));
      let originalSetter = Cu.unwaiveXrays(target.__lookupSetter__(name));

      Object.defineProperty(target, name, {
        get: function(...args) {
          if (!originalGetter) return undefined;
          let result = Cu.waiveXrays(originalGetter.apply(this, args));

          if (self._recording) {
            let timestamp = self.tabActor.window.performance.now() - self._frameStartTimestamp;
            let stack = getStack(name);
            let type = CallWatcherFront.GETTER_FUNCTION;
            callback(unwrappedWindow, global, this, type, name, stack, timestamp, args, result);
          }
          return result;
        },
        set: function(...args) {
          if (!originalSetter) return;
          originalSetter.apply(this, args);

          if (self._recording) {
            let timestamp = self.tabActor.window.performance.now() - self._frameStartTimestamp;
            let stack = getStack(name);
            let type = CallWatcherFront.SETTER_FUNCTION;
            callback(unwrappedWindow, global, this, type, name, stack, timestamp, args, undefined);
          }
        },
        configurable: descriptor.configurable,
        enumerable: descriptor.enumerable
      });
    }

    /**
     * Stores the relevant information about calls on the stack when
     * a function is called.
     */
    function getStack(caller) {
      try {
        // Using Components.stack wouldn't be a better idea, since it's
        // much slower because it attempts to retrieve the C++ stack as well.
        throw new Error();
      } catch (e) {
        var stack = e.stack;
      }

      // Of course, using a simple regex like /(.*?)@(.*):(\d*):\d*/ would be
      // much prettier, but this is a very hot function, so let's sqeeze
      // every drop of performance out of it.
      let calls = [];
      let callIndex = 0;
      let currNewLinePivot = stack.indexOf("\n") + 1;
      let nextNewLinePivot = stack.indexOf("\n", currNewLinePivot);

      while (nextNewLinePivot > 0) {
        let nameDelimiterIndex = stack.indexOf("@", currNewLinePivot);
        let columnDelimiterIndex = stack.lastIndexOf(":", nextNewLinePivot - 1);
        let lineDelimiterIndex = stack.lastIndexOf(":", columnDelimiterIndex - 1);

        if (!calls[callIndex]) {
          calls[callIndex] = { name: "", file: "", line: 0 };
        }
        if (!calls[callIndex + 1]) {
          calls[callIndex + 1] = { name: "", file: "", line: 0 };
        }

        if (callIndex > 0) {
          let file = stack.substring(nameDelimiterIndex + 1, lineDelimiterIndex);
          let line = stack.substring(lineDelimiterIndex + 1, columnDelimiterIndex);
          let name = stack.substring(currNewLinePivot, nameDelimiterIndex);
          calls[callIndex].name = name;
          calls[callIndex - 1].file = file;
          calls[callIndex - 1].line = line;
        } else {
          // Since the topmost stack frame is actually our overwritten function,
          // it will not have the expected name.
          calls[0].name = caller;
        }

        currNewLinePivot = nextNewLinePivot + 1;
        nextNewLinePivot = stack.indexOf("\n", currNewLinePivot);
        callIndex++;
      }

      return calls;
    }
  },

  /**
   * Invoked whenever the current tab actor's inner window is destroyed.
   */
  _onGlobalDestroyed: function({window, id, isTopLevel}) {
    if (this._tracedWindowId == id) {
      this.pauseRecording();
      this.eraseRecording();
    }
  },

  /**
   * Invoked whenever an instrumented function is called.
   */
  _onContentFunctionCall: function(...details) {
    // If the consuming tool has finalized call-watcher, ignore the
    // still-instrumented calls.
    if (this._finalized) {
      return;
    }
    let functionCall = new FunctionCallActor(this.conn, details, this._holdWeak);

    if (this._storeCalls) {
      this._functionCalls.push(functionCall);
    }

    this.onCall(functionCall);
  }
});

/**
 * The corresponding Front object for the CallWatcherActor.
 */
var CallWatcherFront = exports.CallWatcherFront = protocol.FrontClass(CallWatcherActor, {
  initialize: function(client, { callWatcherActor }) {
    protocol.Front.prototype.initialize.call(this, client, { actor: callWatcherActor });
    this.manage(this);
  }
});

/**
 * Constants.
 */
CallWatcherFront.METHOD_FUNCTION = 0;
CallWatcherFront.GETTER_FUNCTION = 1;
CallWatcherFront.SETTER_FUNCTION = 2;

CallWatcherFront.GLOBAL_SCOPE = 0;
CallWatcherFront.UNKNOWN_SCOPE = 1;
CallWatcherFront.CANVAS_WEBGL_CONTEXT = 2;
CallWatcherFront.CANVAS_2D_CONTEXT = 3;

CallWatcherFront.ENUM_METHODS = {};
CallWatcherFront.ENUM_METHODS[CallWatcherFront.CANVAS_2D_CONTEXT] = {
  asyncDrawXULElement: [6],
  drawWindow: [6]
};

CallWatcherFront.ENUM_METHODS[CallWatcherFront.CANVAS_WEBGL_CONTEXT] = {
  activeTexture: [0],
  bindBuffer: [0],
  bindFramebuffer: [0],
  bindRenderbuffer: [0],
  bindTexture: [0],
  blendEquation: [0],
  blendEquationSeparate: [0, 1],
  blendFunc: [0, 1],
  blendFuncSeparate: [0, 1, 2, 3],
  bufferData: [0, 1, 2],
  bufferSubData: [0, 1],
  checkFramebufferStatus: [0],
  clear: [0],
  compressedTexImage2D: [0, 2],
  compressedTexSubImage2D: [0, 6],
  copyTexImage2D: [0, 2],
  copyTexSubImage2D: [0],
  createShader: [0],
  cullFace: [0],
  depthFunc: [0],
  disable: [0],
  drawArrays: [0],
  drawElements: [0, 2],
  enable: [0],
  framebufferRenderbuffer: [0, 1, 2],
  framebufferTexture2D: [0, 1, 2],
  frontFace: [0],
  generateMipmap: [0],
  getBufferParameter: [0, 1],
  getParameter: [0],
  getFramebufferAttachmentParameter: [0, 1, 2],
  getProgramParameter: [1],
  getRenderbufferParameter: [0, 1],
  getShaderParameter: [1],
  getShaderPrecisionFormat: [0, 1],
  getTexParameter: [0, 1],
  getVertexAttrib: [1],
  getVertexAttribOffset: [1],
  hint: [0, 1],
  isEnabled: [0],
  pixelStorei: [0],
  readPixels: [4, 5],
  renderbufferStorage: [0, 1],
  stencilFunc: [0],
  stencilFuncSeparate: [0, 1],
  stencilMaskSeparate: [0],
  stencilOp: [0, 1, 2],
  stencilOpSeparate: [0, 1, 2, 3],
  texImage2D: (args) => args.length > 6 ? [0, 2, 6, 7] : [0, 2, 3, 4],
  texParameterf: [0, 1],
  texParameteri: [0, 1, 2],
  texSubImage2D: (args) => args.length === 9 ? [0, 6, 7] : [0, 4, 5],
  vertexAttribPointer: [2]
};

/**
 * A lookup table for cross-referencing flags or properties with their name
 * assuming they look LIKE_THIS most of the time.
 *
 * For example, when gl.clear(gl.COLOR_BUFFER_BIT) is called, the actual passed
 * argument's value is 16384, which we want identified as "COLOR_BUFFER_BIT".
 */
var gEnumRegex = /^[A-Z][A-Z0-9_]+$/;
var gEnumsLookupTable = {};

// These values are returned from errors, or empty values,
// and need to be ignored when checking arguments due to the bitwise math.
var INVALID_ENUMS = [
  "INVALID_ENUM", "NO_ERROR", "INVALID_VALUE", "OUT_OF_MEMORY", "NONE"
];

function getBitToEnumValue(type, object, arg) {
  let table = gEnumsLookupTable[type];

  // If mapping not yet created, do it on the first run.
  if (!table) {
    table = gEnumsLookupTable[type] = {};

    for (let key in object) {
      if (key.match(gEnumRegex)) {
        // Maps `16384` to `"COLOR_BUFFER_BIT"`, etc.
        table[object[key]] = key;
      }
    }
  }

  // If a single bit value, just return it.
  if (table[arg]) {
    return table[arg];
  }

  // Otherwise, attempt to reduce it to the original bit flags:
  // `16640` -> "COLOR_BUFFER_BIT | DEPTH_BUFFER_BIT"
  let flags = [];
  for (let flag in table) {
    if (INVALID_ENUMS.indexOf(table[flag]) !== -1) {
      continue;
    }

    // Cast to integer as all values are stored as strings
    // in `table`
    flag = flag | 0;
    if (flag && (arg & flag) === flag) {
      flags.push(table[flag]);
    }
  }

  // Cache the combined bitmask value
  return table[arg] = flags.join(" | ") || arg;
}

/**
 * Creates a new error from an error that originated from content but was called
 * from a wrapped overridden method. This is so we can make our own error
 * that does not look like it originated from the call watcher.
 *
 * We use toolkit/loader's parseStack and serializeStack rather than the
 * parsing done in the local `getStack` function, because it does not expose
 * column number, would have to change the protocol models `call-stack-items` and `call-details`
 * which hurts backwards compatibility, and the local `getStack` is an optimized, hot function.
 */
function createContentError (e, win) {
  let { message, name, stack } = e;
  let parsedStack = parseStack(stack);
  let { fileName, lineNumber, columnNumber } = parsedStack[parsedStack.length - 1];
  let error;

  let isDOMException = e instanceof Ci.nsIDOMDOMException;
  let constructor = isDOMException ? win.DOMException : (win[e.name] || win.Error);

  if (isDOMException) {
    error = new constructor(message, name);
    Object.defineProperties(error, {
      code: { value: e.code },
      columnNumber: { value: 0 }, // columnNumber is always 0 for DOMExceptions?
      filename: { value: fileName }, // note the lowercase `filename`
      lineNumber: { value: lineNumber },
      result: { value: e.result },
      stack: { value: serializeStack(parsedStack) }
    });
  }
  else {
    // Constructing an error here retains all the stack information,
    // and we can add message, fileName and lineNumber via constructor, though
    // need to manually add columnNumber.
    error = new constructor(message, fileName, lineNumber);
    Object.defineProperty(error, "columnNumber", {
      value: columnNumber
    });
  }
  return error;
}
