/* 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("resource://devtools/shared/extend.js");
var {
  findPlaceholders,
  getPath,
} = require("resource://devtools/shared/protocol/utils.js");
var { types } = require("resource://devtools/shared/protocol/types.js");

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

exports.Request = Request;

/**
 * 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(arg, ctx) {
    return this.type.write(arg, ctx);
  },

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

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

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