/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* 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 WebConsoleUtils = require("devtools/client/webconsole/utils").Utils;
const STRINGS_URI = "devtools/client/locales/webconsole.properties";
const l10n = new WebConsoleUtils.L10n(STRINGS_URI);

const {
  MESSAGE_SOURCE,
  MESSAGE_TYPE,
  MESSAGE_LEVEL,
} = require("../constants");
const {
  ConsoleMessage,
  NetworkEventMessage,
} = require("../types");

function prepareMessage(packet, idGenerator) {
  // This packet is already in the expected packet structure. Simply return.
  if (!packet.source) {
    packet = transformPacket(packet);
  }

  if (packet.allowRepeating) {
    packet = packet.set("repeatId", getRepeatId(packet));
  }
  return packet.set("id", idGenerator.getNextId());
}

/**
 * Transforms a packet from Firefox RDP structure to Chrome RDP structure.
 */
function transformPacket(packet) {
  if (packet._type) {
    packet = convertCachedPacket(packet);
  }

  switch (packet.type) {
    case "consoleAPICall": {
      let { message } = packet;

      let parameters = message.arguments;
      let type = message.level;
      let level = getLevelFromType(type);
      let messageText = null;
      const timer = message.timer;

      // Special per-type conversion.
      switch (type) {
        case "clear":
          // We show a message to users when calls console.clear() is called.
          parameters = [l10n.getStr("consoleCleared")];
          break;
        case "count":
          // Chrome RDP doesn't have a special type for count.
          type = MESSAGE_TYPE.LOG;
          let {counter} = message;
          let label = counter.label ? counter.label : l10n.getStr("noCounterLabel");
          messageText = `${label}: ${counter.count}`;
          parameters = null;
          break;
        case "time":
          // We don't show anything for console.time calls to match Chrome's behaviour.
          parameters = null;
          type = MESSAGE_TYPE.NULL_MESSAGE;
          break;
        case "timeEnd":
          parameters = null;
          if (timer) {
            // We show the duration to users when calls console.timeEnd() is called,
            // if corresponding console.time() was called before.
            let duration = Math.round(timer.duration * 100) / 100;
            messageText = l10n.getFormatStr("timeEnd", [timer.name, duration]);
          } else {
            // If the `timer` property does not exists, we don't output anything.
            type = MESSAGE_TYPE.NULL_MESSAGE;
          }
          break;
        case "table":
          const supportedClasses = [
            "Array", "Object", "Map", "Set", "WeakMap", "WeakSet"];
          if (
            !Array.isArray(parameters) ||
            parameters.length === 0 ||
            !supportedClasses.includes(parameters[0].class)
          ) {
            // If the class of the first parameter is not supported,
            // we handle the call as a simple console.log
            type = "log";
          }
          break;
        case "group":
          type = MESSAGE_TYPE.START_GROUP;
          parameters = null;
          messageText = message.groupName || l10n.getStr("noGroupLabel");
          break;
        case "groupCollapsed":
          type = MESSAGE_TYPE.START_GROUP_COLLAPSED;
          parameters = null;
          messageText = message.groupName || l10n.getStr("noGroupLabel");
          break;
        case "groupEnd":
          type = MESSAGE_TYPE.END_GROUP;
          parameters = null;
          break;
        case "dirxml":
          // Handle console.dirxml calls as simple console.log
          type = "log";
          break;
      }

      const frame = message.filename ? {
        source: message.filename,
        line: message.lineNumber,
        column: message.columnNumber,
      } : null;

      return new ConsoleMessage({
        source: MESSAGE_SOURCE.CONSOLE_API,
        type,
        level,
        parameters,
        messageText,
        stacktrace: message.stacktrace ? message.stacktrace : null,
        frame,
        userProvidedStyles: message.styles,
      });
    }

    case "navigationMessage": {
      let { message } = packet;
      return new ConsoleMessage({
        source: MESSAGE_SOURCE.CONSOLE_API,
        type: MESSAGE_TYPE.LOG,
        level: MESSAGE_LEVEL.LOG,
        messageText: "Navigated to " + message.url,
      });
    }

    case "pageError": {
      let { pageError } = packet;
      let level = MESSAGE_LEVEL.ERROR;
      if (pageError.warning || pageError.strict) {
        level = MESSAGE_LEVEL.WARN;
      } else if (pageError.info) {
        level = MESSAGE_LEVEL.INFO;
      }

      const frame = pageError.sourceName ? {
        source: pageError.sourceName,
        line: pageError.lineNumber,
        column: pageError.columnNumber
      } : null;

      return new ConsoleMessage({
        source: MESSAGE_SOURCE.JAVASCRIPT,
        type: MESSAGE_TYPE.LOG,
        level,
        messageText: pageError.errorMessage,
        stacktrace: pageError.stacktrace ? pageError.stacktrace : null,
        frame,
        exceptionDocURL: pageError.exceptionDocURL,
        notes: pageError.notes,
      });
    }

    case "networkEvent": {
      let { networkEvent } = packet;

      return new NetworkEventMessage({
        actor: networkEvent.actor,
        isXHR: networkEvent.isXHR,
        request: networkEvent.request,
        response: networkEvent.response,
      });
    }

    case "evaluationResult":
    default: {
      let {
        exceptionMessage: messageText,
        exceptionDocURL,
        frame,
        result: parameters,
        notes,
      } = packet;

      const level = messageText ? MESSAGE_LEVEL.ERROR : MESSAGE_LEVEL.LOG;
      return new ConsoleMessage({
        source: MESSAGE_SOURCE.JAVASCRIPT,
        type: MESSAGE_TYPE.RESULT,
        level,
        messageText,
        parameters,
        exceptionDocURL,
        frame,
        notes,
      });
    }
  }
}

// Helpers
function getRepeatId(message) {
  message = message.toJS();
  delete message.repeat;
  return JSON.stringify(message);
}

function convertCachedPacket(packet) {
  // The devtools server provides cached message packets in a different shape, so we
  // transform them here.
  let convertPacket = {};
  if (packet._type === "ConsoleAPI") {
    convertPacket.message = packet;
    convertPacket.type = "consoleAPICall";
  } else if (packet._type === "PageError") {
    convertPacket.pageError = packet;
    convertPacket.type = "pageError";
  } else if ("_navPayload" in packet) {
    convertPacket.type = "navigationMessage";
    convertPacket.message = packet;
  } else if (packet._type === "NetworkEvent") {
    convertPacket.networkEvent = packet;
    convertPacket.type = "networkEvent";
  } else {
    throw new Error("Unexpected packet type");
  }
  return convertPacket;
}

/**
 * Maps a Firefox RDP type to its corresponding level.
 */
function getLevelFromType(type) {
  const levels = {
    LEVEL_ERROR: "error",
    LEVEL_WARNING: "warn",
    LEVEL_INFO: "info",
    LEVEL_LOG: "log",
    LEVEL_DEBUG: "debug",
  };

  // A mapping from the console API log event levels to the Web Console levels.
  const levelMap = {
    error: levels.LEVEL_ERROR,
    exception: levels.LEVEL_ERROR,
    assert: levels.LEVEL_ERROR,
    warn: levels.LEVEL_WARNING,
    info: levels.LEVEL_INFO,
    log: levels.LEVEL_LOG,
    clear: levels.LEVEL_LOG,
    trace: levels.LEVEL_LOG,
    table: levels.LEVEL_LOG,
    debug: levels.LEVEL_LOG,
    dir: levels.LEVEL_LOG,
    dirxml: levels.LEVEL_LOG,
    group: levels.LEVEL_LOG,
    groupCollapsed: levels.LEVEL_LOG,
    groupEnd: levels.LEVEL_LOG,
    time: levels.LEVEL_LOG,
    timeEnd: levels.LEVEL_LOG,
    count: levels.LEVEL_DEBUG,
  };

  return levelMap[type] || MESSAGE_TYPE.LOG;
}

function isGroupType(type) {
  return [
    MESSAGE_TYPE.START_GROUP,
    MESSAGE_TYPE.START_GROUP_COLLAPSED
  ].includes(type);
}

exports.prepareMessage = prepareMessage;
// Export for use in testing.
exports.getRepeatId = getRepeatId;

exports.l10n = l10n;
exports.isGroupType = isGroupType;
