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

var { Actor } = require("resource://devtools/shared/protocol/Actor.js");
var {
  lazyLoadSpec,
  lazyLoadFront,
} = require("resource://devtools/shared/specs/index.js");

/**
 * 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());

exports.registeredTypes = registeredTypes;

// Values used in specification's request or response attributes to specify
// if a request should only send (request) orreceive (response) raw byte thanks to bulk requests.
// In such case, the front/actor can't send/receive any custom json attribute.
// The request will result into a one-way JSON packet with "actor", "type" and "length" attributes only.
// (i.e. there is no response packet as being one-way)
exports.BULK_REQUEST = Symbol("request");
exports.BULK_RESPONSE = Symbol("response");

/**
 * 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 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);
    }

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

  // Not a collection, might be actor detail
  const pieces = type.split("#", 2);
  if (pieces.length > 1) {
    if (pieces[1] != "actorid") {
      throw new Error(
        "Unsupported detail, only support 'actorid', got: " + pieces[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.
 *
 * @returns a type object that can be used in protocol definitions.
 */
types.addType = function (name, typeObject = {}) {
  if (registeredTypes.has(name)) {
    throw Error("Type '" + name + "' already exists.");
  }

  const type = Object.assign(
    {
      toString() {
        return "[protocol type:" + 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;
      // `ctx.conn` is a DevToolsClient
      let front = ctx.conn.getFrontByID(actorID);

      // When the type `${name}#actorid` is used, `v` is a string refering to the
      // actor ID. We cannot read form information in this case and the actorID was
      // already set when creating the front, so no need to do anything.
      let form = null;
      if (detail != "actorid") {
        form = identityWrite(v);
      }

      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);
        }

        const parentFront = ctx.marshallPool();
        const targetFront = parentFront.isTargetFront
          ? parentFront
          : parentFront.targetFront;

        // Use intermediate Class variable to please eslint requiring
        // a capital letter for all constructors.
        const Class = type.frontClass;
        front = new Class(ctx.conn, targetFront, parentFront);
        front.actorID = actorID;

        parentFront.manage(front, form, ctx);
      } else if (form) {
        front.form(form, 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.isDestroyed()) {
          throw new Error(
            `Attempted to write a response containing a destroyed actor`
          );
        }
        if (!v.actorID) {
          ctx.marshallPool().manage(v);
        }
        if (detail == "actorid") {
          return v.actorID;
        }
        return identityWrite(v.form(detail));
      }

      // Writing a request from the client side, just send the actor id.
      return v.actorID;
    },
  });
  return type;
};

types.addPolymorphicType = function (name, subtypes) {
  // Assert that all subtypes are actors, as the marshalling implementation depends on that.
  for (const subTypeName of subtypes) {
    const subtype = types.getType(subTypeName);
    if (subtype.category != "actor") {
      throw new Error(
        `In polymorphic type '${subtypes.join(
          ","
        )}', the type '${subTypeName}' isn't an actor`
      );
    }
  }

  return types.addType(name, {
    category: "polymorphic",
    read: (value, ctx) => {
      // `value` is either a string which is an Actor ID or a form object
      // where `actor` is an actor ID
      const actorID = typeof value === "string" ? value : value.actor;
      if (!actorID) {
        throw new Error(
          `Was expecting one of these actors '${subtypes}' but instead got value: '${value}'`
        );
      }

      // Extract the typeName out of the actor ID, which should be composed like this
      // ${DevToolsServerConnectionPrefix}.${typeName}${Number}
      const typeName = actorID.match(/\.([a-zA-Z]+)\d+$/)[1];
      if (!subtypes.includes(typeName)) {
        throw new Error(
          `Was expecting one of these actors '${subtypes}' but instead got an actor of type: '${typeName}'`
        );
      }

      const subtype = types.getType(typeName);
      return subtype.read(value, ctx);
    },
    write: (value, ctx) => {
      if (!value) {
        throw new Error(
          `Was expecting one of these actors '${subtypes}' but instead got an empty value.`
        );
      }
      // value is either an `Actor` or a `Front` and both classes exposes a `typeName`
      const typeName = value.typeName;
      if (!typeName) {
        throw new Error(
          `Was expecting one of these actors '${subtypes}' but instead got value: '${value}'. Did you pass a form instead of an Actor?`
        );
      }

      if (!subtypes.includes(typeName)) {
        throw new Error(
          `Was expecting one of these actors '${subtypes}' but instead got an actor of type: '${typeName}'`
        );
      }

      const subtype = types.getType(typeName);
      return subtype.write(value, ctx);
    },
  });
};
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),
  });
};

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

exports.registerFront = function (cls) {
  const { typeName } = cls.prototype;
  if (!registeredTypes.has(typeName)) {
    types.addActorType(typeName);
  }
  registeredTypes.get(typeName).frontClass = cls;
};

/**
 * Instantiate a front of the given type.
 *
 * @param DevToolsClient client
 *    The DevToolsClient instance to use.
 * @param string typeName
 *    The type name of the front to instantiate. This is defined in its specifiation.
 * @returns Front
 *    The created front.
 */
function createFront(client, typeName, target = null) {
  const type = types.getType(typeName);
  if (!type) {
    throw new Error(`No spec for front type '${typeName}'.`);
  } else if (!type.frontClass) {
    lazyLoadFront(typeName);
  }

  // Use intermediate Class variable to please eslint requiring
  // a capital letter for all constructors.
  const Class = type.frontClass;
  return new Class(client, target, target);
}

/**
 * Instantiate a global (preference, device) or target-scoped (webconsole, inspector)
 * front of the given type by picking its actor ID out of either the target or root
 * front's form.
 *
 * @param DevToolsClient client
 *    The DevToolsClient instance to use.
 * @param string typeName
 *    The type name of the front to instantiate. This is defined in its specifiation.
 * @param json form
 *    If we want to instantiate a global actor's front, this is the root front's form,
 *    otherwise we are instantiating a target-scoped front from the target front's form.
 * @param [Target|null] target
 *    If we are instantiating a target-scoped front, this is a reference to the front's
 *    Target instance, otherwise this is null.
 */
async function getFront(client, typeName, form, target = null) {
  const front = createFront(client, typeName, target);
  const { formAttributeName } = front;
  if (!formAttributeName) {
    throw new Error(`Can't find the form attribute name for ${typeName}`);
  }
  // Retrieve the actor ID from root or target actor's form
  front.actorID = form[formAttributeName];
  if (!front.actorID) {
    throw new Error(
      `Can't find the actor ID for ${typeName} from root or target` +
        ` actor's form.`
    );
  }

  if (!target) {
    await front.manage(front);
  } else {
    await target.manage(front);
  }

  return front;
}
exports.getFront = getFront;

/**
 * Create a RootFront.
 *
 * @param DevToolsClient client
 *    The DevToolsClient instance to use.
 * @param Object packet
 * @returns RootFront
 */
function createRootFront(client, packet) {
  const rootFront = createFront(client, "root");
  rootFront.form(packet);

  // Root Front is a special case, managing itself as it doesn't have any parent.
  // It will register itself to DevToolsClient as a Pool via Front._poolMap.
  rootFront.manage(rootFront);

  return rootFront;
}
exports.createRootFront = createRootFront;
