/* 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 { extend } = require("devtools/shared/extend");
var EventEmitter = require("devtools/shared/event-emitter");
var {getStack, callFunctionWithAsyncStack} = require("devtools/shared/platform/stack");
var {settleAll} = require("devtools/shared/DevToolsUtils");
var {lazyLoadSpec, lazyLoadFront} = require("devtools/shared/specs/index");

// Bug 1454373: devtools/shared/defer still uses Promise.jsm which is slower
// than DOM Promises. So implement our own copy of `defer` based on DOM Promises.
function defer() {
  let resolve, reject;
  const promise = new Promise(function() {
    resolve = arguments[0];
    reject = arguments[1];
  });
  return {
    resolve: resolve,
    reject: reject,
    promise: promise
  };
}

/**
 * Types: named marshallers/demarshallers.
 *
 * Types provide a 'write' function that takes a js representation and
 * returns a protocol representation, and a "read" function that
 * takes a protocol representation and returns a js representation.
 *
 * The read and write methods are also passed a context object that
 * represent the actor or front requesting the translation.
 *
 * Types are referred to with a typestring.  Basic types are
 * registered by name using addType, and more complex types can
 * be generated by adding detail to the type name.
 */

var types = Object.create(null);
exports.types = types;

var registeredTypes = types.registeredTypes = new Map();
var registeredLifetimes = types.registeredLifetimes = new Map();

/**
 * Return the type object associated with a given typestring.
 * If passed a type object, it will be returned unchanged.
 *
 * Types can be registered with addType, or can be created on
 * the fly with typestrings.  Examples:
 *
 *   boolean
 *   threadActor
 *   threadActor#detail
 *   array:threadActor
 *   array:array:threadActor#detail
 *
 * @param [typestring|type] type
 *    Either a typestring naming a type or a type object.
 *
 * @returns a type object.
 */
types.getType = function(type) {
  if (!type) {
    return types.Primitive;
  }

  if (typeof (type) !== "string") {
    return type;
  }

  // If already registered, we're done here.
  let reg = registeredTypes.get(type);
  if (reg) {
    return reg;
  }

  // Try to lazy load the spec, if not already loaded.
  if (lazyLoadSpec(type)) {
    // If a spec module was lazy loaded, it will synchronously call
    // generateActorSpec, and set the type in `registeredTypes`.
    reg = registeredTypes.get(type);
    if (reg) {
      return reg;
    }
  }

  // New type, see if it's a collection/lifetime type:
  const sep = type.indexOf(":");
  if (sep >= 0) {
    const collection = type.substring(0, sep);
    const subtype = types.getType(type.substring(sep + 1));

    if (collection === "array") {
      return types.addArrayType(subtype);
    } else if (collection === "nullable") {
      return types.addNullableType(subtype);
    }

    if (registeredLifetimes.has(collection)) {
      return types.addLifetimeType(collection, subtype);
    }

    throw Error("Unknown collection type: " + collection);
  }

  // Not a collection, might be actor detail
  const pieces = type.split("#", 2);
  if (pieces.length > 1) {
    return types.addActorDetail(type, pieces[0], pieces[1]);
  }

  throw Error("Unknown type: " + type);
};

/**
 * Don't allow undefined when writing primitive types to packets.  If
 * you want to allow undefined, use a nullable type.
 */
function identityWrite(v) {
  if (v === undefined) {
    throw Error("undefined passed where a value is required");
  }
  // This has to handle iterator->array conversion because arrays of
  // primitive types pass through here.
  if (v && typeof v.next === "function") {
    return [...v];
  }
  return v;
}

/**
 * Add a type to the type system.
 *
 * When registering a type, you can provide `read` and `write` methods.
 *
 * The `read` method will be passed a JS object value from the JSON
 * packet and must return a native representation.  The `write` method will
 * be passed a native representation and should provide a JSONable value.
 *
 * These methods will both be passed a context.  The context is the object
 * performing or servicing the request - on the server side it will be
 * an Actor, on the client side it will be a Front.
 *
 * @param typestring name
 *    Name to register
 * @param object typeObject
 *    An object whose properties will be stored in the type, including
 *    the `read` and `write` methods.
 * @param object options
 *    Can specify `thawed` to prevent the type from being frozen.
 *
 * @returns a type object that can be used in protocol definitions.
 */
types.addType = function(name, typeObject = {}, options = {}) {
  if (registeredTypes.has(name)) {
    throw Error("Type '" + name + "' already exists.");
  }

  const type = Object.assign({
    toString() {
      return "[protocol type:" + name + "]";
    },
    name: name,
    primitive: !(typeObject.read || typeObject.write),
    read: identityWrite,
    write: identityWrite
  }, typeObject);

  registeredTypes.set(name, type);

  return type;
};

/**
 * Remove a type previously registered with the system.
 * Primarily useful for types registered by addons.
 */
types.removeType = function(name) {
  // This type may still be referenced by other types, make sure
  // those references don't work.
  const type = registeredTypes.get(name);

  type.name = "DEFUNCT:" + name;
  type.category = "defunct";
  type.primitive = false;
  type.read = type.write = function() {
    throw new Error("Using defunct type: " + name);
  };

  registeredTypes.delete(name);
};

/**
 * Add an array type to the type system.
 *
 * getType() will call this function if provided an "array:<type>"
 * typestring.
 *
 * @param type subtype
 *    The subtype to be held by the array.
 */
types.addArrayType = function(subtype) {
  subtype = types.getType(subtype);

  const name = "array:" + subtype.name;

  // Arrays of primitive types are primitive types themselves.
  if (subtype.primitive) {
    return types.addType(name);
  }
  return types.addType(name, {
    category: "array",
    read: (v, ctx) => {
      if (v && typeof v.next === "function") {
        v = [...v];
      }
      return v.map(i => subtype.read(i, ctx));
    },
    write: (v, ctx) => {
      if (v && typeof v.next === "function") {
        v = [...v];
      }
      return v.map(i => subtype.write(i, ctx));
    }
  });
};

/**
 * Add a dict type to the type system.  This allows you to serialize
 * a JS object that contains non-primitive subtypes.
 *
 * Properties of the value that aren't included in the specializations
 * will be serialized as primitive values.
 *
 * @param object specializations
 *    A dict of property names => type
 */
types.addDictType = function(name, specializations) {
  const specTypes = {};
  for (const prop in specializations) {
    try {
      specTypes[prop] = types.getType(specializations[prop]);
    } catch (e) {
      // Types may not be defined yet. Sometimes, we define the type *after* using it, but
      // also, we have cyclic definitions on types. So lazily load them when they are not
      // immediately available.
      loader.lazyGetter(specTypes, prop, () => {
        return types.getType(specializations[prop]);
      });
    }
  }
  return types.addType(name, {
    category: "dict",
    specializations,
    read: (v, ctx) => {
      const ret = {};
      for (const prop in v) {
        if (prop in specTypes) {
          ret[prop] = specTypes[prop].read(v[prop], ctx);
        } else {
          ret[prop] = v[prop];
        }
      }
      return ret;
    },

    write: (v, ctx) => {
      const ret = {};
      for (const prop in v) {
        if (prop in specTypes) {
          ret[prop] = specTypes[prop].write(v[prop], ctx);
        } else {
          ret[prop] = v[prop];
        }
      }
      return ret;
    }
  });
};

/**
 * Register an actor type with the type system.
 *
 * Types are marshalled differently when communicating server->client
 * than they are when communicating client->server.  The server needs
 * to provide useful information to the client, so uses the actor's
 * `form` method to get a json representation of the actor.  When
 * making a request from the client we only need the actor ID string.
 *
 * This function can be called before the associated actor has been
 * constructed, but the read and write methods won't work until
 * the associated addActorImpl or addActorFront methods have been
 * called during actor/front construction.
 *
 * @param string name
 *    The typestring to register.
 */
types.addActorType = function(name) {
  // We call addActorType from:
  //   FrontClassWithSpec when registering front synchronously,
  //   generateActorSpec when defining specs,
  //   specs modules to register actor type early to use them in other types
  if (registeredTypes.has(name)) {
    return registeredTypes.get(name);
  }
  const type = types.addType(name, {
    _actor: true,
    category: "actor",
    read: (v, ctx, detail) => {
      // If we're reading a request on the server side, just
      // find the actor registered with this actorID.
      if (ctx instanceof Actor) {
        return ctx.conn.getActor(v);
      }

      // Reading a response on the client side, check for an
      // existing front on the connection, and create the front
      // if it isn't found.
      const actorID = typeof (v) === "string" ? v : v.actor;
      let front = ctx.conn.getActor(actorID);
      if (!front) {
        // If front isn't instantiated yet, create one.

        // Try lazy loading front if not already loaded.
        // The front module will synchronously call `FrontClassWithSpec` and
        // augment `type` with the `frontClass` attribute.
        if (!type.frontClass) {
          lazyLoadFront(name);
        }

        front = new type.frontClass(ctx.conn); // eslint-disable-line new-cap
        front.actorID = actorID;
        ctx.marshallPool().manage(front);
      }

      v = type.formType(detail).read(v, front, detail);
      front.form(v, detail, ctx);

      return front;
    },
    write: (v, ctx, detail) => {
      // If returning a response from the server side, make sure
      // the actor is added to a parent object and return its form.
      if (v instanceof Actor) {
        if (!v.actorID) {
          ctx.marshallPool().manage(v);
        }
        return type.formType(detail).write(v.form(detail), ctx, detail);
      }

      // Writing a request from the client side, just send the actor id.
      return v.actorID;
    },
    formType: (detail) => {
      if (!("formType" in type.actorSpec)) {
        return types.Primitive;
      }

      let formAttr = "formType";
      if (detail) {
        formAttr += "#" + detail;
      }

      if (!(formAttr in type.actorSpec)) {
        throw new Error("No type defined for " + formAttr);
      }

      return type.actorSpec[formAttr];
    }
  });
  return type;
};

types.addNullableType = function(subtype) {
  subtype = types.getType(subtype);
  return types.addType("nullable:" + subtype.name, {
    category: "nullable",
    read: (value, ctx) => {
      if (value == null) {
        return value;
      }
      return subtype.read(value, ctx);
    },
    write: (value, ctx) => {
      if (value == null) {
        return value;
      }
      return subtype.write(value, ctx);
    }
  });
};

/**
 * Register an actor detail type.  This is just like an actor type, but
 * will pass a detail hint to the actor's form method during serialization/
 * deserialization.
 *
 * This is called by getType() when passed an 'actorType#detail' string.
 *
 * @param string name
 *   The typestring to register this type as.
 * @param type actorType
 *   The actor type you'll be detailing.
 * @param string detail
 *   The detail to pass.
 */
types.addActorDetail = function(name, actorType, detail) {
  actorType = types.getType(actorType);
  if (!actorType._actor) {
    throw Error(`Details only apply to actor types, tried to add detail '${detail}' ` +
                `to ${actorType.name}`);
  }
  return types.addType(name, {
    _actor: true,
    category: "detail",
    read: (v, ctx) => actorType.read(v, ctx, detail),
    write: (v, ctx) => actorType.write(v, ctx, detail)
  });
};

/**
 * Register an actor lifetime.  This lets the type system find a parent
 * actor that differs from the actor fulfilling the request.
 *
 * @param string name
 *    The lifetime name to use in typestrings.
 * @param string prop
 *    The property of the actor that holds the parent that should be used.
 */
types.addLifetime = function(name, prop) {
  if (registeredLifetimes.has(name)) {
    throw Error("Lifetime '" + name + "' already registered.");
  }
  registeredLifetimes.set(name, prop);
};

/**
 * Remove a previously-registered lifetime.  Useful for lifetimes registered
 * in addons.
 */
types.removeLifetime = function(name) {
  registeredLifetimes.delete(name);
};

/**
 * Register a lifetime type.  This creates an actor type tied to the given
 * lifetime.
 *
 * This is called by getType() when passed a '<lifetimeType>:<actorType>'
 * typestring.
 *
 * @param string lifetime
 *    A lifetime string previously regisered with addLifetime()
 * @param type subtype
 *    An actor type
 */
types.addLifetimeType = function(lifetime, subtype) {
  subtype = types.getType(subtype);
  if (!subtype._actor) {
    throw Error(`Lifetimes only apply to actor types, tried to apply ` +
                `lifetime '${lifetime}' to ${subtype.name}`);
  }
  const prop = registeredLifetimes.get(lifetime);
  return types.addType(lifetime + ":" + subtype.name, {
    category: "lifetime",
    read: (value, ctx) => subtype.read(value, ctx[prop]),
    write: (value, ctx) => subtype.write(value, ctx[prop])
  });
};

// Add a few named primitive types.
types.Primitive = types.addType("primitive");
types.String = types.addType("string");
types.Number = types.addType("number");
types.Boolean = types.addType("boolean");
types.JSON = types.addType("json");

/**
 * Request/Response templates and generation
 *
 * Request packets are specified as json templates with
 * Arg and Option placeholders where arguments should be
 * placed.
 *
 * Reponse packets are also specified as json templates,
 * with a RetVal placeholder where the return value should be
 * placed.
 */

/**
 * Placeholder for simple arguments.
 *
 * @param number index
 *    The argument index to place at this position.
 * @param type type
 *    The argument should be marshalled as this type.
 * @constructor
 */
var Arg = function(index, type) {
  this.index = index;
  // Prevent force loading all Arg types by accessing it only when needed
  loader.lazyGetter(this, "type", function() {
    return types.getType(type);
  });
};

Arg.prototype = {
  write: function(arg, ctx) {
    return this.type.write(arg, ctx);
  },

  read: function(v, ctx, outArgs) {
    outArgs[this.index] = this.type.read(v, ctx);
  },

  describe: function() {
    return {
      _arg: this.index,
      type: this.type.name,
    };
  }
};

// Outside of protocol.js, Arg is called as factory method, without the new keyword.
exports.Arg = function(index, type) {
  return new Arg(index, type);
};

/**
 * Placeholder for an options argument value that should be hoisted
 * into the packet.
 *
 * If provided in a method specification:
 *
 *   { optionArg: Option(1)}
 *
 * Then arguments[1].optionArg will be placed in the packet in this
 * value's place.
 *
 * @param number index
 *    The argument index of the options value.
 * @param type type
 *    The argument should be marshalled as this type.
 * @constructor
 */
var Option = function(index, type) {
  Arg.call(this, index, type);
};

Option.prototype = extend(Arg.prototype, {
  write: function(arg, ctx, name) {
    // Ignore if arg is undefined or null; allow other falsy values
    if (arg == undefined || arg[name] == undefined) {
      return undefined;
    }
    const v = arg[name];
    return this.type.write(v, ctx);
  },
  read: function(v, ctx, outArgs, name) {
    if (outArgs[this.index] === undefined) {
      outArgs[this.index] = {};
    }
    if (v === undefined) {
      return;
    }
    outArgs[this.index][name] = this.type.read(v, ctx);
  },

  describe: function() {
    return {
      _option: this.index,
      type: this.type.name,
    };
  }
});

// Outside of protocol.js, Option is called as factory method, without the new keyword.
exports.Option = function(index, type) {
  return new Option(index, type);
};

/**
 * Placeholder for return values in a response template.
 *
 * @param type type
 *    The return value should be marshalled as this type.
 */
var RetVal = function(type) {
  // Prevent force loading all RetVal types by accessing it only when needed
  loader.lazyGetter(this, "type", function() {
    return types.getType(type);
  });
};

RetVal.prototype = {
  write: function(v, ctx) {
    return this.type.write(v, ctx);
  },

  read: function(v, ctx) {
    return this.type.read(v, ctx);
  },

  describe: function() {
    return {
      _retval: this.type.name
    };
  }
};

// Outside of protocol.js, RetVal is called as factory method, without the new keyword.
exports.RetVal = function(type) {
  return new RetVal(type);
};

/* Template handling functions */

/**
 * Get the value at a given path, or undefined if not found.
 */
function getPath(obj, path) {
  for (const name of path) {
    if (!(name in obj)) {
      return undefined;
    }
    obj = obj[name];
  }
  return obj;
}

/**
 * Find Placeholders in the template and save them along with their
 * paths.
 */
function findPlaceholders(template, constructor, path = [], placeholders = []) {
  if (!template || typeof (template) != "object") {
    return placeholders;
  }

  if (template instanceof constructor) {
    placeholders.push({ placeholder: template, path: [...path] });
    return placeholders;
  }

  for (const name in template) {
    path.push(name);
    findPlaceholders(template[name], constructor, path, placeholders);
    path.pop();
  }

  return placeholders;
}

function describeTemplate(template) {
  return JSON.parse(JSON.stringify(template, (key, value) => {
    if (value.describe) {
      return value.describe();
    }
    return value;
  }));
}

/**
 * Manages a request template.
 *
 * @param object template
 *    The request template.
 * @construcor
 */
var Request = function(template = {}) {
  this.type = template.type;
  this.template = template;
  this.args = findPlaceholders(template, Arg);
};

Request.prototype = {
  /**
   * Write a request.
   *
   * @param array fnArgs
   *    The function arguments to place in the request.
   * @param object ctx
   *    The object making the request.
   * @returns a request packet.
   */
  write: function(fnArgs, ctx) {
    const ret = {};
    for (const key in this.template) {
      const value = this.template[key];
      if (value instanceof Arg) {
        ret[key] = value.write(value.index in fnArgs ? fnArgs[value.index] : undefined,
                               ctx, key);
      } else if (key == "type") {
        ret[key] = value;
      } else {
        throw new Error("Request can only an object with `Arg` or `Option` properties");
      }
    }
    return ret;
  },

  /**
   * Read a request.
   *
   * @param object packet
   *    The request packet.
   * @param object ctx
   *    The object making the request.
   * @returns an arguments array
   */
  read: function(packet, ctx) {
    const fnArgs = [];
    for (const templateArg of this.args) {
      const arg = templateArg.placeholder;
      const path = templateArg.path;
      const name = path[path.length - 1];
      arg.read(getPath(packet, path), ctx, fnArgs, name);
    }
    return fnArgs;
  },

  describe: function() {
    return describeTemplate(this.template);
  }
};

/**
 * Manages a response template.
 *
 * @param object template
 *    The response template.
 * @construcor
 */
var Response = function(template = {}) {
  this.template = template;
  const placeholders = findPlaceholders(template, RetVal);
  if (placeholders.length > 1) {
    throw Error("More than one RetVal specified in response");
  }
  const placeholder = placeholders.shift();
  if (placeholder) {
    this.retVal = placeholder.placeholder;
    this.path = placeholder.path;
  }
};

Response.prototype = {
  /**
   * Write a response for the given return value.
   *
   * @param val ret
   *    The return value.
   * @param object ctx
   *    The object writing the response.
   */
  write: function(ret, ctx) {
    // Consider that `template` is either directly a `RetVal`,
    // or a dictionary with may be one `RetVal`.
    if (this.template instanceof RetVal) {
      return this.template.write(ret, ctx);
    }
    const result = {};
    for (const key in this.template) {
      const value = this.template[key];
      if (value instanceof RetVal) {
        result[key] = value.write(ret, ctx);
      } else {
        throw new Error("Response can only be a `RetVal` instance or an object " +
                        "with one property being a `RetVal` instance.");
      }
    }
    return result;
  },

  /**
   * Read a return value from the given response.
   *
   * @param object packet
   *    The response packet.
   * @param object ctx
   *    The object reading the response.
   */
  read: function(packet, ctx) {
    if (!this.retVal) {
      return undefined;
    }
    const v = getPath(packet, this.path);
    return this.retVal.read(v, ctx);
  },

  describe: function() {
    return describeTemplate(this.template);
  }
};

/**
 * Actor and Front implementations
 */

/**
 * A protocol object that can manage the lifetime of other protocol
 * objects. Pools are used on both sides of the connection to help coordinate lifetimes.
 *
 * @param optional conn
 *   Either a DebuggerServerConnection or a DebuggerClient.  Must have
 *   addActorPool, removeActorPool, and poolFor.
 *   conn can be null if the subclass provides a conn property.
 * @constructor
 */
var Pool = function(conn) {
  if (conn) {
    this.conn = conn;
  }
};

Pool.prototype = extend(EventEmitter.prototype, {
  /**
   * Return the parent pool for this client.
   */
  parent: function() {
    return this.conn.poolFor(this.actorID);
  },

  /**
   * Override this if you want actors returned by this actor
   * to belong to a different actor by default.
   */
  marshallPool: function() {
    return this;
  },

  /**
   * Pool is the base class for all actors, even leaf nodes.
   * If the child map is actually referenced, go ahead and create
   * the stuff needed by the pool.
   */
  __poolMap: null,
  get _poolMap() {
    if (this.__poolMap) {
      return this.__poolMap;
    }
    this.__poolMap = new Map();
    this.conn.addActorPool(this);
    return this.__poolMap;
  },

  /**
   * Add an actor as a child of this pool.
   */
  manage: function(actor) {
    if (!actor.actorID) {
      actor.actorID = this.conn.allocID(actor.actorPrefix || actor.typeName);
    }

    this._poolMap.set(actor.actorID, actor);
    return actor;
  },

  /**
   * Remove an actor as a child of this pool.
   */
  unmanage: function(actor) {
    this.__poolMap && this.__poolMap.delete(actor.actorID);
  },

  // true if the given actor ID exists in the pool.
  has: function(actorID) {
    return this.__poolMap && this._poolMap.has(actorID);
  },

  // The actor for a given actor id stored in this pool
  actor: function(actorID) {
    return this.__poolMap ? this._poolMap.get(actorID) : null;
  },

  // Same as actor, should update debugger connection to use 'actor'
  // and then remove this.
  get: function(actorID) {
    return this.__poolMap ? this._poolMap.get(actorID) : null;
  },

  // True if this pool has no children.
  isEmpty: function() {
    return !this.__poolMap || this._poolMap.size == 0;
  },

  // Generator that yields each non-self child of the pool.
  poolChildren: function* () {
    if (!this.__poolMap) {
      return;
    }
    for (const actor of this.__poolMap.values()) {
      // Self-owned actors are ok, but don't need visiting twice.
      if (actor === this) {
        continue;
      }
      yield actor;
    }
  },

  /**
   * Destroy this item, removing it from a parent if it has one,
   * and destroying all children if necessary.
   */
  destroy: function() {
    const parent = this.parent();
    if (parent) {
      parent.unmanage(this);
    }
    if (!this.__poolMap) {
      return;
    }
    for (const actor of this.__poolMap.values()) {
      // Self-owned actors are ok, but don't need destroying twice.
      if (actor === this) {
        continue;
      }
      const destroy = actor.destroy;
      if (destroy) {
        // Disconnect destroy while we're destroying in case of (misbehaving)
        // circular ownership.
        actor.destroy = null;
        destroy.call(actor);
        actor.destroy = destroy;
      }
    }
    this.conn.removeActorPool(this, true);
    this.__poolMap.clear();
    this.__poolMap = null;
  },

  /**
   * For getting along with the debugger server pools, should be removable
   * eventually.
   */
  cleanup: function() {
    this.destroy();
  }
});
exports.Pool = Pool;

/**
 * Keep track of which actorSpecs have been created. If a replica of a spec
 * is created, it can be caught, and specs which inherit from other specs will
 * not overwrite eachother.
 */
var actorSpecs = new WeakMap();

/**
 * An actor in the actor tree.
 *
 * @param optional conn
 *   Either a DebuggerServerConnection or a DebuggerClient.  Must have
 *   addActorPool, removeActorPool, and poolFor.
 *   conn can be null if the subclass provides a conn property.
 * @constructor
 */
var Actor = function(conn) {
  Pool.call(this, conn);

  this._actorSpec = actorSpecs.get(Object.getPrototypeOf(this));
  // Forward events to the connection.
  if (this._actorSpec && this._actorSpec.events) {
    for (const [name, request] of this._actorSpec.events.entries()) {
      this.on(name, (...args) => {
        this._sendEvent(name, request, ...args);
      });
    }
  }
};

Actor.prototype = extend(Pool.prototype, {
  // Will contain the actor's ID
  actorID: null,

  // Existing Actors extending this class expect initialize to contain constructor logic.
  initialize: Actor,

  toString: function() {
    return "[Actor " + this.typeName + "/" + this.actorID + "]";
  },

  _sendEvent: function(name, request, ...args) {
    let packet;
    try {
      packet = request.write(args, this);
    } catch (ex) {
      console.error("Error sending event: " + name);
      throw ex;
    }
    packet.from = packet.from || this.actorID;
    this.conn.send(packet);
  },

  destroy: function() {
    Pool.prototype.destroy.call(this);
    this.actorID = null;
  },

  /**
   * Override this method in subclasses to serialize the actor.
   * @param [optional] string hint
   *   Optional string to customize the form.
   * @returns A jsonable object.
   */
  form: function(hint) {
    return { actor: this.actorID };
  },

  writeError: function(error, typeName, method) {
    console.error(`Error while calling actor '${typeName}'s method '${method}'`,
                  error.message);
    if (error.stack) {
      console.error(error.stack);
    }
    this.conn.send({
      from: this.actorID,
      error: error.error || "unknownError",
      message: error.message
    });
  },

  _queueResponse: function(create) {
    const pending = this._pendingResponse || Promise.resolve(null);
    const response = create(pending);
    this._pendingResponse = response;
  }
});
exports.Actor = Actor;

/**
 * Tags a prtotype method as an actor method implementation.
 *
 * @param function fn
 *    The implementation function, will be returned.
 * @param spec
 *    The method specification, with the following (optional) properties:
 *      request (object): a request template.
 *      response (object): a response template.
 *      oneway (bool): 'true' if no response should be sent.
 */
exports.method = function(fn, spec = {}) {
  fn._methodSpec = Object.freeze(spec);
  if (spec.request) {
    Object.freeze(spec.request);
  }
  if (spec.response) {
    Object.freeze(spec.response);
  }
  return fn;
};

/**
 * Generates an actor specification from an actor description.
 */
var generateActorSpec = function(actorDesc) {
  const actorSpec = {
    typeName: actorDesc.typeName,
    methods: []
  };

  // Find method and form specifications attached to properties.
  for (const name of Object.getOwnPropertyNames(actorDesc)) {
    const desc = Object.getOwnPropertyDescriptor(actorDesc, name);
    if (!desc.value) {
      continue;
    }

    if (name.startsWith("formType")) {
      if (typeof (desc.value) === "string") {
        actorSpec[name] = types.getType(desc.value);
      } else if (desc.value.name && registeredTypes.has(desc.value.name)) {
        actorSpec[name] = desc.value;
      } else {
        // Shorthand for a newly-registered DictType.
        actorSpec[name] = types.addDictType(actorDesc.typeName + "__" + name, desc.value);
      }
    }

    if (desc.value._methodSpec) {
      const methodSpec = desc.value._methodSpec;
      const spec = {};
      spec.name = methodSpec.name || name;
      spec.request = new Request(Object.assign({type: spec.name},
                                          methodSpec.request || undefined));
      spec.response = new Response(methodSpec.response || undefined);
      spec.release = methodSpec.release;
      spec.oneway = methodSpec.oneway;

      actorSpec.methods.push(spec);
    }
  }

  // Find additional method specifications
  if (actorDesc.methods) {
    for (const name in actorDesc.methods) {
      const methodSpec = actorDesc.methods[name];
      const spec = {};

      spec.name = methodSpec.name || name;
      spec.request = new Request(Object.assign({type: spec.name},
                                          methodSpec.request || undefined));
      spec.response = new Response(methodSpec.response || undefined);
      spec.release = methodSpec.release;
      spec.oneway = methodSpec.oneway;

      actorSpec.methods.push(spec);
    }
  }

  // Find event specifications
  if (actorDesc.events) {
    actorSpec.events = new Map();
    for (const name in actorDesc.events) {
      const eventRequest = actorDesc.events[name];
      Object.freeze(eventRequest);
      actorSpec.events.set(name, new Request(Object.assign({type: name}, eventRequest)));
    }
  }

  if (!registeredTypes.has(actorSpec.typeName)) {
    types.addActorType(actorSpec.typeName);
  }
  registeredTypes.get(actorSpec.typeName).actorSpec = actorSpec;

  return actorSpec;
};
exports.generateActorSpec = generateActorSpec;

/**
 * Generates request handlers as described by the given actor specification on
 * the given actor prototype. Returns the actor prototype.
 */
var generateRequestHandlers = function(actorSpec, actorProto) {
  actorProto.typeName = actorSpec.typeName;

  // Generate request handlers for each method definition
  actorProto.requestTypes = Object.create(null);
  actorSpec.methods.forEach(spec => {
    const handler = function(packet, conn) {
      try {
        let args;
        try {
          args = spec.request.read(packet, this);
        } catch (ex) {
          console.error("Error reading request: " + packet.type);
          throw ex;
        }

        if (!this[spec.name]) {
          throw new Error(`Spec for '${actorProto.typeName}' specifies a '${spec.name}'` +
                          ` method that isn't implemented by the actor`);
        }
        const ret = this[spec.name].apply(this, args);

        const sendReturn = (retToSend) => {
          if (spec.oneway) {
            // No need to send a response.
            return;
          }

          let response;
          try {
            response = spec.response.write(retToSend, this);
          } catch (ex) {
            console.error("Error writing response to: " + spec.name);
            throw ex;
          }
          response.from = this.actorID;
          // If spec.release has been specified, destroy the object.
          if (spec.release) {
            try {
              this.destroy();
            } catch (e) {
              this.writeError(e, actorProto.typeName, spec.name);
              return;
            }
          }

          conn.send(response);
        };

        this._queueResponse(p => {
          return p
            .then(() => ret)
            .then(sendReturn)
            .catch(e => this.writeError(e, actorProto.typeName, spec.name));
        });
      } catch (e) {
        this._queueResponse(p => {
          return p.then(() => this.writeError(e, actorProto.typeName, spec.name));
        });
      }
    };

    actorProto.requestTypes[spec.request.type] = handler;
  });

  return actorProto;
};

/**
 * Create an actor class for the given actor specification and prototype.
 *
 * @param object actorSpec
 *    The actor specification. Must have a 'typeName' property.
 * @param object actorProto
 *    The actor prototype. Should have method definitions, can have event
 *    definitions.
 */
var ActorClassWithSpec = function(actorSpec, actorProto) {
  if (!actorSpec.typeName) {
    throw Error("Actor specification must have a typeName member.");
  }

  // Existing Actors are relying on the initialize instead of constructor methods.
  const cls = function() {
    const instance = Object.create(cls.prototype);
    instance.initialize.apply(instance, arguments);
    return instance;
  };
  cls.prototype = extend(Actor.prototype, generateRequestHandlers(actorSpec, actorProto));

  actorSpecs.set(cls.prototype, actorSpec);

  return cls;
};
exports.ActorClassWithSpec = ActorClassWithSpec;

/**
 * Base class for client-side actor fronts.
 *
 * @param optional conn
 *   Either a DebuggerServerConnection or a DebuggerClient.  Must have
 *   addActorPool, removeActorPool, and poolFor.
 *   conn can be null if the subclass provides a conn property.
 * @param optional form
 *   The json form provided by the server.
 * @constructor
 */
var Front = function(conn = null, form = null, detail = null, context = null) {
  Pool.call(this, conn);
  this._requests = [];

  // protocol.js no longer uses this data in the constructor, only external
  // uses do.  External usage of manually-constructed fronts will be
  // drastically reduced if we convert the root and target actors to
  // protocol.js, in which case this can probably go away.
  if (form) {
    this.actorID = form.actor;
    form = types.getType(this.typeName).formType(detail).read(form, this, detail);
    this.form(form, detail, context);
  }
};

Front.prototype = extend(Pool.prototype, {
  actorID: null,

  // Existing Fronts extending this class expect initialize to contain constructor logic.
  initialize: Front,

  destroy: function() {
    // Reject all outstanding requests, they won't make sense after
    // the front is destroyed.
    while (this._requests && this._requests.length > 0) {
      const { deferred, to, type, stack } = this._requests.shift();
      const msg = "Connection closed, pending request to " + to +
                ", type " + type + " failed" +
                "\n\nRequest stack:\n" + stack.formattedStack;
      deferred.reject(new Error(msg));
    }
    Pool.prototype.destroy.call(this);
    this.actorID = null;
  },

  manage: function(front) {
    if (!front.actorID) {
      throw new Error("Can't manage front without an actor ID.\n" +
                      "Ensure server supports " + front.typeName + ".");
    }
    return Pool.prototype.manage.call(this, front);
  },

  /**
   * @returns a promise that will resolve to the actorID this front
   * represents.
   */
  actor: function() {
    return Promise.resolve(this.actorID);
  },

  toString: function() {
    return "[Front for " + this.typeName + "/" + this.actorID + "]";
  },

  /**
   * Update the actor from its representation.
   * Subclasses should override this.
   */
  form: function(form) {},

  /**
   * Send a packet on the connection.
   */
  send: function(packet) {
    if (packet.to) {
      this.conn._transport.send(packet);
    } else {
      this.actor().then(actorID => {
        packet.to = actorID;
        this.conn._transport.send(packet);
      }).catch(console.error);
    }
  },

  /**
   * Send a two-way request on the connection.
   */
  request: function(packet) {
    const deferred = defer();
    // Save packet basics for debugging
    const { to, type } = packet;
    this._requests.push({
      deferred,
      to: to || this.actorID,
      type,
      stack: getStack(),
    });
    this.send(packet);
    return deferred.promise;
  },

  /**
   * Handler for incoming packets from the client's actor.
   */
  onPacket: function(packet) {
    // Pick off event packets
    const type = packet.type || undefined;
    if (this._clientSpec.events && this._clientSpec.events.has(type)) {
      const event = this._clientSpec.events.get(packet.type);
      let args;
      try {
        args = event.request.read(packet, this);
      } catch (ex) {
        console.error("Error reading event: " + packet.type);
        console.exception(ex);
        throw ex;
      }
      if (event.pre) {
        const results = event.pre.map(pre => pre.apply(this, args));

        // Check to see if any of the preEvents returned a promise -- if so,
        // wait for their resolution before emitting. Otherwise, emit synchronously.
        if (results.some(result => result && typeof result.then === "function")) {
          Promise.all(results).then(() => {
            return EventEmitter.emit.apply(null, [this, event.name].concat(args));
          });
          return;
        }
      }

      EventEmitter.emit.apply(null, [this, event.name].concat(args));
      return;
    }

    // Remaining packets must be responses.
    if (this._requests.length === 0) {
      const msg = "Unexpected packet " + this.actorID + ", " + JSON.stringify(packet);
      const err = Error(msg);
      console.error(err);
      throw err;
    }

    const { deferred, stack } = this._requests.shift();
    callFunctionWithAsyncStack(() => {
      if (packet.error) {
        // "Protocol error" is here to avoid TBPL heuristics. See also
        // https://dxr.mozilla.org/webtools-central/source/tbpl/php/inc/GeneralErrorFilter.php
        let message;
        if (packet.error && packet.message) {
          message = "Protocol error (" + packet.error + "): " + packet.message;
        } else {
          message = packet.error;
        }
        deferred.reject(message);
      } else {
        deferred.resolve(packet);
      }
    }, stack, "DevTools RDP");
  },

  hasRequests() {
    return !!this._requests.length;
  },

  /**
   * Wait for all current requests from this front to settle.  This is especially useful
   * for tests and other utility environments that may not have events or mechanisms to
   * await the completion of requests without this utility.
   *
   * @return Promise
   *         Resolved when all requests have settled.
   */
  waitForRequestsToSettle() {
    return settleAll(this._requests.map(({ deferred }) => deferred.promise));
  },
});

exports.Front = Front;

/**
 * A method tagged with preEvent will be called after recieving a packet
 * for that event, and before the front emits the event.
 */
exports.preEvent = function(eventName, fn) {
  fn._preEvent = eventName;
  return fn;
};

/**
 * Mark a method as a custom front implementation, replacing the generated
 * front method.
 *
 * @param function fn
 *    The front implementation, will be returned.
 * @param object options
 *    Options object:
 *      impl (string): If provided, the generated front method will be
 *        stored as this property on the prototype.
 */
exports.custom = function(fn, options = {}) {
  fn._customFront = options;
  return fn;
};

/**
 * Generates request methods as described by the given actor specification on
 * the given front prototype. Returns the front prototype.
 */
var generateRequestMethods = function(actorSpec, frontProto) {
  if (frontProto._actorSpec) {
    throw new Error("frontProto called twice on the same front prototype!");
  }

  frontProto.typeName = actorSpec.typeName;

  // Generate request methods.
  const methods = actorSpec.methods;
  methods.forEach(spec => {
    let name = spec.name;

    // If there's already a property by this name in the front, it must
    // be a custom front method.
    if (name in frontProto) {
      const custom = frontProto[spec.name]._customFront;
      if (custom === undefined) {
        throw Error(`Existing method for ${spec.name} not marked customFront while ` +
                    ` processing ${actorSpec.typeName}.`);
      }
      // If the user doesn't need the impl don't generate it.
      if (!custom.impl) {
        return;
      }
      name = custom.impl;
    }

    frontProto[name] = function(...args) {
      let packet;
      try {
        packet = spec.request.write(args, this);
      } catch (ex) {
        console.error("Error writing request: " + name);
        throw ex;
      }
      if (spec.oneway) {
        // Fire-and-forget oneway packets.
        this.send(packet);
        return undefined;
      }

      return this.request(packet).then(response => {
        let ret;
        try {
          ret = spec.response.read(response, this);
        } catch (ex) {
          console.error("Error reading response to: " + name);
          throw ex;
        }
        return ret;
      });
    };

    // Release methods should call the destroy function on return.
    if (spec.release) {
      const fn = frontProto[name];
      frontProto[name] = function(...args) {
        return fn.apply(this, args).then(result => {
          this.destroy();
          return result;
        });
      };
    }
  });

  // Process event specifications
  frontProto._clientSpec = {};

  const actorEvents = actorSpec.events;
  if (actorEvents) {
    // This actor has events, scan the prototype for preEvent handlers...
    const preHandlers = new Map();
    for (const name of Object.getOwnPropertyNames(frontProto)) {
      const desc = Object.getOwnPropertyDescriptor(frontProto, name);
      if (!desc.value) {
        continue;
      }
      if (desc.value._preEvent) {
        const preEvent = desc.value._preEvent;
        if (!actorEvents.has(preEvent)) {
          throw Error("preEvent for event that doesn't exist: " + preEvent);
        }
        let handlers = preHandlers.get(preEvent);
        if (!handlers) {
          handlers = [];
          preHandlers.set(preEvent, handlers);
        }
        handlers.push(desc.value);
      }
    }

    frontProto._clientSpec.events = new Map();

    for (const [name, request] of actorEvents) {
      frontProto._clientSpec.events.set(request.type, {
        name: name,
        request: request,
        pre: preHandlers.get(name)
      });
    }
  }

  frontProto._actorSpec = actorSpec;

  return frontProto;
};

/**
 * Create a front class for the given actor specification and front prototype.
 *
 * @param object actorSpec
 *    The actor specification you're creating a front for.
 * @param object proto
 *    The object prototype.  Must have a 'typeName' property,
 *    should have method definitions, can have event definitions.
 */
var FrontClassWithSpec = function(actorSpec, frontProto) {
  // Existing Fronts are relying on the initialize instead of constructor methods.
  const cls = function() {
    const instance = Object.create(cls.prototype);
    instance.initialize.apply(instance, arguments);
    return instance;
  };
  cls.prototype = extend(Front.prototype, generateRequestMethods(actorSpec, frontProto));

  if (!registeredTypes.has(actorSpec.typeName)) {
    types.addActorType(actorSpec.typeName);
  }
  registeredTypes.get(actorSpec.typeName).frontClass = cls;

  return cls;
};
exports.FrontClassWithSpec = FrontClassWithSpec;

exports.dumpActorSpec = function(type) {
  const actorSpec = type.actorSpec;
  const ret = {
    category: "actor",
    typeName: type.name,
    methods: [],
    events: {}
  };

  for (const method of actorSpec.methods) {
    ret.methods.push({
      name: method.name,
      release: method.release || undefined,
      oneway: method.oneway || undefined,
      request: method.request.describe(),
      response: method.response.describe()
    });
  }

  if (actorSpec.events) {
    for (const [name, request] of actorSpec.events) {
      ret.events[name] = request.describe();
    }
  }

  JSON.stringify(ret);

  return ret;
};

exports.dumpProtocolSpec = function() {
  const ret = {
    types: {},
  };

  for (let [name, type] of registeredTypes) {
    // Force lazy instantiation if needed.
    type = types.getType(name);
    const category = type.category || undefined;
    if (category === "dict") {
      ret.types[name] = {
        category: "dict",
        typeName: name,
        specializations: type.specializations
      };
    } else if (category === "actor") {
      ret.types[name] = exports.dumpActorSpec(type);
    }
  }

  return ret;
};
