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

const logger = console.createInstance({
  prefix: "devtools_rdp",
  maxLogLevel: "Warn",
});

// Hack MOZ_LOG/Console.cpp usage of ToSource logic
// to be able to write raw strings to stdout.
// This prevents being wrapped with quotes, and allow to use ANSI color codes.
const SEND_MOZ_LOG_SYMBOL = {
  toSource() {
    return " \x1b[2m->\x1b[0m ";
  },
};
let colorCounter = 0;

/**
 * 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 => {
    const { name } = spec;

    frontProto[name] = function (...args) {
      // If the front is destroyed, the request will not be able to complete.
      if (this.isDestroyed()) {
        throw new Error(
          `Can not send request '${name}' because front '${this.typeName}' is already destroyed.`
        );
      }

      const startTime = ChromeUtils.now();
      let packet;
      try {
        packet = spec.request.write(args, this);
      } catch (ex) {
        console.error("Error writing request: " + name);
        throw ex;
      }
      if (spec.oneway) {
        // Log outgoing RDP packet being sent via Protocol.js
        // (packet sent via DevToolsClient will be logged from DevToolsClient codebase)
        logger.log(SEND_MOZ_LOG_SYMBOL, packet);

        // Fire-and-forget oneway packets.
        this.send(packet);
        return undefined;
      }

      // Check if the client request should be sent as a bulk request
      const isSendingBulkData = spec.request.template === BULK_REQUEST;

      // If so, pass the last front argument as the bulk initialization callback
      const clientBulkCallback = isSendingBulkData ? args.at(-1) : null;

      // For each incoming request, we will rotate through the first 15 existing ANSI colors
      // which are all quite visible and different from each others.
      // Note that colors are specific to each request and not each front.
      let color;
      if (logger.shouldLog("Log")) {
        color = 1 + (colorCounter % 15);
        colorCounter++;
        logger.log(
          {
            toSource() {
              return `\x1b[38;5;${color}m->\x1b[0m`;
            },
          },
          // Ensure adding the `to` attribute which will be later set by request method.
          { ...packet, to: this.actorID }
        );
      }

      return this.request(packet, {
        bulk: isSendingBulkData,
        clientBulkCallback,
      }).then(response => {
        // If the request returns bulk data, return the transport response as-is.
        // We do not expect any custom packet/attributes for bulk responses,
        // the transport will handle the binary stream communication and expose
        // the StreamCopier as resolution value in the returned Promise.
        const isReceivingBulkData = spec.response.template === BULK_RESPONSE;
        if (isReceivingBulkData) {
          return response;
        }
        if (logger.shouldLog("Log")) {
          logger.log(
            {
              toSource() {
                return `\x1b[38;5;${color}m<-\x1b[0m`;
              },
            },
            response
          );
        }

        let ret;
        if (!this.conn) {
          throw new Error("Missing conn on " + this);
        }
        if (this.isDestroyed()) {
          throw new Error(
            `Can not interpret '${name}' response because front '${this.typeName}' is already destroyed.`
          );
        }
        try {
          ret = spec.response.read(response, this);
        } catch (ex) {
          console.error("Error reading response to: " + name + "\n" + ex);
          throw ex;
        }
        ChromeUtils.addProfilerMarker(
          "RDP Front",
          startTime,
          `${this.typeName}:${name}()`
        );
        return ret;
      });
    };

    // Release methods should call the destroy function on return.
    if (spec.release) {
      const fn = frontProto[name];
      frontProto[name] = async function (...args) {
        // Note that if release is oneway, fn.apply won't be async,
        // nor return any value.
        const result = await fn.apply(this, args);
        this.destroy();
        return result;
      };
    }
  });

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

  const actorEvents = actorSpec.events;
  if (actorEvents) {
    frontProto._clientSpec.events = new Map();

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

  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) {
  class OneFront extends Front {}
  generateRequestMethods(actorSpec, OneFront.prototype);
  return OneFront;
};
exports.FrontClassWithSpec = FrontClassWithSpec;
