/* 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/. */

import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";

const lazy = {};

ChromeUtils.defineESModuleGetters(lazy, {
  Log: "chrome://remote/content/shared/Log.sys.mjs",
});

XPCOMUtils.defineLazyGetter(lazy, "logger", () =>
  lazy.Log.get(lazy.Log.TYPES.CDP)
);

export class RemoteAgentError extends Error {
  constructor(message = "", cause = undefined) {
    cause = cause || message;
    super(cause);

    this.name = this.constructor.name;
    this.message = message;
    this.cause = cause;

    this.notify();
  }

  notify() {
    console.error(this);
    lazy.logger.error(this.toString({ stack: true }));
  }

  toString({ stack = false } = {}) {
    return RemoteAgentError.format(this, { stack });
  }

  static format(e, { stack = false } = {}) {
    return formatError(e, { stack });
  }

  /**
   * Takes a serialised CDP error and reconstructs it
   * as a RemoteAgentError.
   *
   * The error must be of this form:
   *
   *     {"message": "TypeError: foo is not a function\n
   *     execute@chrome://remote/content/cdp/sessions/Session.jsm:73:39\n
   *     onMessage@chrome://remote/content/cdp/sessions/TabSession.jsm:65:20"}
   *
   * This approach has the notable deficiency that it cannot deal
   * with causes to errors because of the unstructured nature of CDP
   * errors.  A possible future improvement would be to extend the
   * error serialisation to include discrete fields for each data
   * property.
   *
   * @param {object} json
   *     CDP error encoded as a JSON object, which must have a
   *     "message" field, where the first line will make out the error
   *     message and the subsequent lines the stacktrace.
   *
   * @returns {RemoteAgentError}
   */
  static fromJSON(json) {
    const [message, ...stack] = json.message.split("\n");
    const err = new RemoteAgentError();
    err.message = message.slice(0, -1);
    err.stack = stack.map(s => s.trim()).join("\n");
    err.cause = null;
    return err;
  }
}

/**
 * A fatal error that it is not possible to recover from
 * or send back to the client.
 *
 * Constructing this error will force the application to quit.
 */
export class FatalError extends RemoteAgentError {
  constructor(...args) {
    super(...args);
    this.quit();
  }

  notify() {
    lazy.logger.fatal(this.toString({ stack: true }));
  }

  quit(mode = Ci.nsIAppStartup.eForceQuit) {
    Services.startup.quit(mode);
  }
}

/** When an operation is not yet implemented. */
export class UnsupportedError extends RemoteAgentError {}

/** The requested remote method does not exist. */
export class UnknownMethodError extends RemoteAgentError {
  constructor(domain, command = null) {
    if (command) {
      super(`${domain}.${command}`);
    } else {
      super(domain);
    }
  }
}

function formatError(error, { stack = false } = {}) {
  const els = [];

  els.push(error.name);
  if (error.message) {
    els.push(": ");
    els.push(error.message);
  }

  if (stack && error.stack) {
    els.push(":\n");

    const stack = error.stack.trim().split("\n");
    els.push(stack.map(line => `\t${line}`).join("\n"));

    if (error.cause) {
      els.push("\n");
      els.push("caused by: " + formatError(error.cause, { stack }));
    }
  }

  return els.join("");
}
