/* -*- indent-tabs-mode: nil; js-indent-level: 2; js-indent-level: 2 -*- */
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* 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 protocol = require("devtools/server/protocol");
const { method, Arg, Option, RetVal } = protocol;

const {DebuggerServer} = require("devtools/server/main");

/**
 * Error Messages
 */

const ERR_DIRECTOR_INSTALL_TWICE = "Trying to install a director-script twice";
const ERR_DIRECTOR_INSTALL_EMPTY = "Trying to install an empty director-script";
const ERR_DIRECTOR_UNINSTALL_UNKNOWN = "Trying to uninstall an unkown director-script";

const ERR_DIRECTOR_PARENT_UNKNOWN_METHOD = "Unknown parent process method";
const ERR_DIRECTOR_CHILD_NOTIMPLEMENTED_METHOD = "Unexpected call to notImplemented method";
const ERR_DIRECTOR_CHILD_MULTIPLE_REPLIES = "Unexpected multiple replies to called parent method";
const ERR_DIRECTOR_CHILD_NO_REPLY = "Unexpected no reply to called parent method";

/**
 * Director Registry
 */

// Map of director scripts ids to director script definitions
var gDirectorScripts = Object.create(null);

const DirectorRegistry = exports.DirectorRegistry = {
  /**
   * Register a Director Script with the debugger server.
   * @param id string
   *    The ID of a director script.
   * @param directorScriptDef object
   *    The definition of a director script.
   */
  install: function (id, scriptDef) {
    if (id in gDirectorScripts) {
      console.error(ERR_DIRECTOR_INSTALL_TWICE,id);
      return false;
    }

    if (!scriptDef) {
      console.error(ERR_DIRECTOR_INSTALL_EMPTY, id);
      return false;
    }

    gDirectorScripts[id] = scriptDef;

    return true;
  },

  /**
   * Unregister a Director Script with the debugger server.
   * @param id string
   *    The ID of a director script.
   */
  uninstall: function(id) {
    if (id in gDirectorScripts) {
      delete gDirectorScripts[id];

      return true;
    }

    console.error(ERR_DIRECTOR_UNINSTALL_UNKNOWN, id);

    return false;
  },

  /**
   * Returns true if a director script id has been registered.
   * @param id string
   *    The ID of a director script.
   */
  checkInstalled: function (id) {
    return (this.list().indexOf(id) >= 0);
  },

  /**
   * Returns a registered director script definition by id.
   * @param id string
   *    The ID of a director script.
   */
  get: function(id) {
    return gDirectorScripts[id];
  },

  /**
   * Returns an array of registered director script ids.
   */
  list: function() {
    return Object.keys(gDirectorScripts);
  },

  /**
   * Removes all the registered director scripts.
   */
  clear: function() {
   gDirectorScripts = Object.create(null);
  }
};

/**
 * E10S parent/child setup helpers
 */

let gTrackedMessageManager = new Set();

exports.setupParentProcess = function setupParentProcess({mm, childID}) {
  // prevents multiple subscriptions on the same messagemanager
  if (gTrackedMessageManager.has(mm)) {
    return;
  }
  gTrackedMessageManager.add(mm);

  // listen for director-script requests from the child process
  mm.addMessageListener("debug:director-registry-request", handleChildRequest);

  DebuggerServer.once("disconnected-from-child:" + childID, handleMessageManagerDisconnected);

  /* parent process helpers */

  function handleMessageManagerDisconnected(evt, { mm: disconnected_mm }) {
    // filter out not subscribed message managers
    if (disconnected_mm !== mm || !gTrackedMessageManager.has(mm)) {
      return;
    }

    gTrackedMessageManager.delete(mm);

    // unregister for director-script requests handlers from the parent process (if any)
    mm.removeMessageListener("debug:director-registry-request", handleChildRequest);
  }

  function handleChildRequest(msg) {
    switch (msg.json.method) {
    case "get":
      return DirectorRegistry.get(msg.json.args[0]);
    case "list":
      return DirectorRegistry.list();
    default:
      console.error(ERR_DIRECTOR_PARENT_UNKNOWN_METHOD, msg.json.method);
      throw new Error(ERR_DIRECTOR_PARENT_UNKNOWN_METHOD);
    }
  }
};

// skip child setup if this actor module is not running in a child process
if (DebuggerServer.isInChildProcess) {
  setupChildProcess();
}

function setupChildProcess() {
  const { sendSyncMessage } = DebuggerServer.parentMessageManager;

  DebuggerServer.setupInParent({
    module: "devtools/server/actors/director-registry",
    setupParent: "setupParentProcess"
  });

  DirectorRegistry.install = notImplemented.bind(null, "install");
  DirectorRegistry.uninstall = notImplemented.bind(null, "uninstall");
  DirectorRegistry.clear = notImplemented.bind(null, "clear");

  DirectorRegistry.get = callParentProcess.bind(null, "get");
  DirectorRegistry.list = callParentProcess.bind(null, "list");

  /* child process helpers */

  function notImplemented(method) {
    console.error(ERR_DIRECTOR_CHILD_NOTIMPLEMENTED_METHOD, method);
    throw Error(ERR_DIRECTOR_CHILD_NOTIMPLEMENTED_METHOD);
  }

  function callParentProcess(method, ...args) {
    var reply = sendSyncMessage("debug:director-registry-request", {
      method: method,
      args: args
    });

    if (reply.length === 0) {
      console.error(ERR_DIRECTOR_CHILD_NO_REPLY);
      throw Error(ERR_DIRECTOR_CHILD_NO_REPLY);
    } else if (reply.length > 1) {
      console.error(ERR_DIRECTOR_CHILD_MULTIPLE_REPLIES);
      throw Error(ERR_DIRECTOR_CHILD_MULTIPLE_REPLIES);
    }

    return reply[0];
  };
};

/**
 * The DirectorRegistry Actor is a global actor which manages install/uninstall of
 * director scripts definitions.
 */
const DirectorRegistryActor = exports.DirectorRegistryActor = protocol.ActorClass({
  typeName: "director-registry",

  /* init & destroy methods */
  initialize: function(conn, parentActor) {
    protocol.Actor.prototype.initialize.call(this, conn);
  },
  destroy: function(conn) {
    protocol.Actor.prototype.destroy.call(this, conn);
    this.finalize();
  },

  finalize: method(function() {
    // nothing to cleanup
  }, {
    oneway: true
  }),

  /**
   * Install a new director-script definition.
   *
   * @param String id
   *        The director-script definition identifier.
   * @param String scriptCode
   *        The director-script javascript source.
   * @param Object scriptOptions
   *        The director-script option object.
   */
  install: method(function(id, { scriptCode, scriptOptions }) {
    // TODO: add more checks on id format?
    if (!id || id.length === 0) {
      throw Error("director-script id is mandatory");
    }

    if (!scriptCode) {
      throw Error("director-script scriptCode is mandatory");
    }

    return DirectorRegistry.install(id, {
      scriptId: id,
      scriptCode: scriptCode,
      scriptOptions: scriptOptions
    });
  }, {
    request: {
      scriptId: Arg(0, "string"),
      scriptCode: Option(1, "string"),
      scriptOptions: Option(1, "nullable:json")
    },
    response: {
      success: RetVal("boolean")
    }
  }),

  /**
   * Uninstall a director-script definition.
   *
   * @param String id
   *        The identifier of the director-script definition to be removed
   */
  uninstall: method(function (id) {
    return DirectorRegistry.uninstall(id);
  }, {
    request: {
      scritpId: Arg(0, "string")
    },
    response: {
      success: RetVal("boolean")
    }
  }),

  /**
   * Retrieves the list of installed director-scripts.
   */
  list: method(function () {
    return DirectorRegistry.list();
  }, {
    response: {
      directorScripts: RetVal("array:string")
    }
  })
});

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