import * as irc from "./irc.js";

// Static list of capabilities that are always requested when supported by the
// server
const permanentCaps = ["message-tags", "server-time", "multi-prefix"];

export default class Client extends EventTarget {
	ws = null;
	nick = null;
	params = {
		url: null,
		username: null,
		realname: null,
		nick: null,
		pass: null,
		saslPlain: null,
	};
	registered = false;
	availableCaps = {};
	enabledCaps = {};

	constructor(params) {
		super();

		this.params = Object.assign(this.params, params);

		try {
			this.ws = new WebSocket(params.url);
		} catch (err) {
			console.error("Failed to create connection:", err);
			setTimeout(() => this.dispatchEvent(new CustomEvent("close")), 0);
			return;
		}

		this.ws.addEventListener("open", this.handleOpen.bind(this));
		this.ws.addEventListener("message", this.handleMessage.bind(this));

		this.ws.addEventListener("close", () => {
			console.log("Connection closed");
			this.dispatchEvent(new CustomEvent("close"));
		});

		this.ws.addEventListener("error", () => {
			console.error("Connection error");
		});
	}

	handleOpen() {
		console.log("Connection opened");

		this.nick = this.params.nick;

		this.send({ command: "CAP", params: ["LS", "302"] });
		if (this.params.pass) {
			this.send({ command: "PASS", params: [this.params.pass] });
		}
		this.send({ command: "NICK", params: [this.nick] });
		this.send({
			command: "USER",
			params: [this.params.username, "0", "*", this.params.realname],
		});
	}

	handleMessage(event) {
		var msg = irc.parseMessage(event.data);
		console.log("Received:", msg);

		switch (msg.command) {
		case irc.RPL_WELCOME:
			if (this.params.saslPlain && this.availableCaps["sasl"] === undefined) {
				console.error("Server doesn't support SASL PLAIN");
				this.close();
				return;
			}

			console.log("Registration complete");
			this.registered = true;
			break;
		case irc.ERR_PASSWDMISMATCH:
			console.error("Password mismatch");
			this.close();
			break;
		case "CAP":
			this.handleCap(msg);
			break;
		case "AUTHENTICATE":
			this.handleAuthenticate(msg);
			break;
		case irc.RPL_LOGGEDIN:
			console.log("Logged in");
			break;
		case irc.RPL_LOGGEDOUT:
			console.log("Logged out");
			break;
		case irc.RPL_SASLSUCCESS:
			console.log("SASL authentication success");
			if (!this.registered) {
				this.send({ command: "CAP", params: ["END"] });
			}
			break;
		case irc.ERR_NICKLOCKED:
		case irc.ERR_SASLFAIL:
		case irc.ERR_SASLTOOLONG:
		case irc.ERR_SASLABORTED:
		case irc.ERR_SASLALREADY:
			console.error("SASL error:", msg);
			this.close();
			break;
		case "NICK":
			var newNick = msg.params[0];
			if (msg.prefix.name == this.nick) {
				this.nick = newNick;
			}
			break;
		}

		this.dispatchEvent(new CustomEvent("message", {
			detail: { message: msg },
		}));
	}

	addAvailableCaps(s) {
		var l = s.split(" ");
		l.forEach((s) => {
			var parts = s.split("=");
			var k = parts[0];
			var v = "";
			if (parts.length > 1) {
				v = parts[1];
			}
			this.availableCaps[k] = v;
		});
	}

	handleCap(msg) {
		var subCmd = msg.params[1];
		var args = msg.params.slice(2);
		switch (subCmd) {
		case "LS":
			this.addAvailableCaps(args[args.length - 1]);
			if (args[0] != "*") {
				console.log("Available server caps:", this.availableCaps);

				var reqCaps = [];

				var saslCap = this.availableCaps["sasl"];
				var supportsSaslPlain = (saslCap !== undefined);
				if (saslCap.length > 0) {
					supportsSaslPlain = saslCap.split(",").includes("PLAIN");
				}

				var capEnd = true;
				if (this.params.saslPlain && supportsSaslPlain) {
					// CAP END is deferred after authentication finishes
					reqCaps.push("sasl");
					capEnd = false;
				}

				permanentCaps.forEach((cap) => {
					if (this.availableCaps[cap] !== undefined) {
						reqCaps.push(cap);
					}
				});

				if (reqCaps.length > 0) {
					this.send({ command: "CAP", params: ["REQ", reqCaps.join(" ")] });
				}

				if (!this.registered && capEnd) {
					this.send({ command: "CAP", params: ["END"] });
				}
			}
			break;
		case "NEW":
			this.addAvailableCaps(args[0]);
			console.log("Server added available caps:", args[0]);
			// TODO: request caps
			break;
		case "DEL":
			args[0].split(" ").forEach((cap) => {
				delete this.availableCaps[cap];
				delete this.enabledCaps[cap];
			});
			console.log("Server removed available caps:", args[0]);
			break;
		case "ACK":
			console.log("Server ack'ed caps:", args[0]);
			args[0].split(" ").forEach((cap) => {
				this.enabledCaps[cap] = true;

				if (cap == "sasl" && this.params.saslPlain) {
					console.log("Starting SASL PLAIN authentication");
					this.send({ command: "AUTHENTICATE", params: ["PLAIN"] });
				}
			});
			break;
		case "NAK":
			console.log("Server nak'ed caps:", args[0]);
			if (!this.registered) {
				this.send({ command: "CAP", params: ["END"] });
			}
			break;
		}
	}

	handleAuthenticate(msg) {
		var challengeStr = msg.params[0];

		// For now only PLAIN is supported
		if (challengeStr != "+") {
			console.error("Expected an empty challenge, got:", challengeStr);
			this.send({ command: "AUTHENTICATE", params: ["*"] });
			return;
		}

		var respStr = btoa("\0" + this.params.saslPlain.username + "\0" + this.params.saslPlain.password);
		this.send({ command: "AUTHENTICATE", params: [respStr] });
	}

	send(msg) {
		this.ws.send(irc.formatMessage(msg));
		console.log("Sent:", msg);
	}

	close() {
		this.ws.close(1000);
		this.registered = false;
	}
}