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

/**
 * This implements the Client-to-Client Protocol (CTCP), a subprotocol of IRC.
 *   REVISED AND UPDATED CTCP SPECIFICATION
 *     http://www.alien.net.au/irc/ctcp.txt
 */

import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
import { l10nHelper } from "resource:///modules/imXPCOMUtils.sys.mjs";
import { ircHandlerPriorities } from "resource:///modules/ircHandlerPriorities.sys.mjs";
import { displayMessage } from "resource:///modules/ircUtils.sys.mjs";

const lazy = {};
XPCOMUtils.defineLazyGetter(lazy, "_", () =>
  l10nHelper("chrome://chat/locale/irc.properties")
);

// Split into a CTCP message which is a single command and a single parameter:
//   <command> " " <parameter>
// The high level dequote is to unescape \001 in the message content.
export function CTCPMessage(aMessage, aRawCTCPMessage) {
  let message = Object.assign({}, aMessage);
  message.ctcp = {};
  message.ctcp.rawMessage = aRawCTCPMessage;

  // High/CTCP level dequote: replace the quote char \134 followed by a or \134
  // with \001 or \134, respectively. Any other character after \134 is replaced
  // with itself.
  let dequotedCTCPMessage = message.ctcp.rawMessage.replace(
    /\\(.|$)/g,
    aStr => {
      if (aStr[1]) {
        return aStr[1] == "a" ? "\x01" : aStr[1];
      }
      return "";
    }
  );

  let separator = dequotedCTCPMessage.indexOf(" ");
  // If there's no space, then only a command is given.
  // Do not capitalize the command, case sensitive
  if (separator == -1) {
    message.ctcp.command = dequotedCTCPMessage;
    message.ctcp.param = "";
  } else {
    message.ctcp.command = dequotedCTCPMessage.slice(0, separator);
    message.ctcp.param = dequotedCTCPMessage.slice(separator + 1);
  }
  return message;
}

// This is the CTCP handler for IRC protocol, it will call each CTCP handler.
export var ircCTCP = {
  name: "CTCP",
  // Slightly above default RFC 2812 priority.
  priority: ircHandlerPriorities.HIGH_PRIORITY,
  isEnabled: () => true,

  // CTCP uses only PRIVMSG and NOTICE commands.
  commands: {
    PRIVMSG: ctcpHandleMessage,
    NOTICE: ctcpHandleMessage,
  },
};

// Parse the message and call all CTCP handlers on the message.
function ctcpHandleMessage(message, ircHandlers) {
  // If there are no CTCP handlers, then don't parse the CTCP message.
  if (!ircHandlers.hasCTCPHandlers) {
    return false;
  }

  // The raw CTCP message is in the last parameter of the IRC message.
  let rawCTCPParam = message.params.slice(-1)[0];

  // Split the raw message into the multiple CTCP messages and pull out the
  // command and parameters.
  let ctcpMessages = [];
  let otherMessage = rawCTCPParam.replace(
    // eslint-disable-next-line no-control-regex
    /\x01([^\x01]*)\x01/g,
    function (aMatch, aMsg) {
      if (aMsg) {
        ctcpMessages.push(new CTCPMessage(message, aMsg));
      }
      return "";
    }
  );

  // If no CTCP messages were found, return false.
  if (!ctcpMessages.length) {
    return false;
  }

  // If there's some message left, send it back through the IRC handlers after
  // stripping out the CTCP information. I highly doubt this will ever happen,
  // but just in case. ;)
  if (otherMessage) {
    message.params.pop();
    message.params.push(otherMessage);
    ircHandlers.handleMessage(this, message);
  }

  // Loop over each raw CTCP message.
  for (let message of ctcpMessages) {
    if (!ircHandlers.handleCTCPMessage(this, message)) {
      this.WARN(
        "Unhandled CTCP message: " +
          message.ctcp.rawMessage +
          "\nin IRC message: " +
          message.rawMessage
      );
      // For unhandled CTCP message, respond with a NOTICE ERRMSG that echoes
      // back the original command.
      this.sendCTCPMessage(message.origin, true, "ERRMSG", [
        message.ctcp.rawMessage,
        ":Unhandled CTCP command",
      ]);
    }
  }

  // We have handled this message as much as we can.
  return true;
}

// This is the the basic CTCP protocol.
export var ctcpBase = {
  // Parameters
  name: "CTCP",
  priority: ircHandlerPriorities.DEFAULT_PRIORITY,
  isEnabled: () => true,

  // These represent CTCP commands.
  commands: {
    ACTION(aMessage) {
      // ACTION <text>
      // Display message in conversation
      return displayMessage(
        this,
        aMessage,
        { action: true },
        aMessage.ctcp.param
      );
    },

    // Used when an error needs to be replied with.
    ERRMSG(aMessage) {
      this.WARN(
        aMessage.origin +
          " failed to handle CTCP message: " +
          aMessage.ctcp.param
      );
      return true;
    },

    // This is commented out since CLIENTINFO automatically returns the
    // supported CTCP parameters and this is not supported.

    // Returns the user's full name, and idle time.
    // "FINGER": function(aMessage) { return false; },

    // Dynamic master index of what a client knows.
    CLIENTINFO(message, ircHandlers) {
      if (message.command == "PRIVMSG") {
        // Received a CLIENTINFO request, respond with the support CTCP
        // messages.
        let info = new Set();
        for (let handler of ircHandlers._ctcpHandlers) {
          for (let command in handler.commands) {
            info.add(command);
          }
        }

        let supportedCtcp = [...info].join(" ");
        this.LOG(
          "Reporting support for the following CTCP messages: " + supportedCtcp
        );
        this.sendCTCPMessage(message.origin, true, "CLIENTINFO", supportedCtcp);
      } else {
        // Received a CLIENTINFO response, store the information for future
        // use.
        let info = message.ctcp.param.split(" ");
        this.setWhois(message.origin, { clientInfo: info });
      }
      return true;
    },

    // Used to measure the delay of the IRC network between clients.
    PING(aMessage) {
      // PING timestamp
      if (aMessage.command == "PRIVMSG") {
        // Received PING request, send PING response.
        this.LOG(
          "Received PING request from " +
            aMessage.origin +
            '. Sending PING response: "' +
            aMessage.ctcp.param +
            '".'
        );
        this.sendCTCPMessage(
          aMessage.origin,
          true,
          "PING",
          aMessage.ctcp.param
        );
        return true;
      }
      return this.handlePingReply(aMessage.origin, aMessage.ctcp.param);
    },

    // These are commented out since CLIENTINFO automatically returns the
    // supported CTCP parameters and this is not supported.

    // An encryption protocol between clients without any known reference.
    // "SED": function(aMessage) { return false; },

    // Where to obtain a copy of a client.
    // "SOURCE": function(aMessage) { return false; },

    // Gets the local date and time from other clients.
    TIME(aMessage) {
      if (aMessage.command == "PRIVMSG") {
        // TIME
        // Received a TIME request, send a human readable response.
        let now = new Date().toString();
        this.LOG(
          "Received TIME request from " +
            aMessage.origin +
            '. Sending TIME response: "' +
            now +
            '".'
        );
        this.sendCTCPMessage(aMessage.origin, true, "TIME", ":" + now);
      } else {
        // TIME :<human-readable-time-string>
        // Received a TIME reply, display it.
        // Remove the : prefix, if it exists and display the result.
        let time = aMessage.ctcp.param.slice(aMessage.ctcp.param[0] == ":");
        this.getConversation(aMessage.origin).writeMessage(
          aMessage.origin,
          lazy._("ctcp.time", aMessage.origin, time),
          { system: true, tags: aMessage.tags }
        );
      }
      return true;
    },

    // This is commented out since CLIENTINFO automatically returns the
    // supported CTCP parameters and this is not supported.

    // A string set by the user (never the client coder)
    // "USERINFO": function(aMessage) { return false; },

    // The version and type of the client.
    VERSION(aMessage) {
      if (aMessage.command == "PRIVMSG") {
        // VERSION
        // Received VERSION request, send VERSION response.
        let version = Services.appinfo.name + " " + Services.appinfo.version;
        this.LOG(
          "Received VERSION request from " +
            aMessage.origin +
            '. Sending VERSION response: "' +
            version +
            '".'
        );
        this.sendCTCPMessage(aMessage.origin, true, "VERSION", version);
      } else if (aMessage.command == "NOTICE" && aMessage.ctcp.param.length) {
        // VERSION #:#:#
        // Received VERSION response, display to the user.
        let response = lazy._(
          "ctcp.version",
          aMessage.origin,
          aMessage.ctcp.param
        );
        this.getConversation(aMessage.origin).writeMessage(
          aMessage.origin,
          response,
          {
            system: true,
            tags: aMessage.tags,
          }
        );
      }
      return true;
    },
  },
};
