"use strict";

// vendor libraries
require("jquery-ui/ui/widgets/sortable");
const $ = require("jquery");
const moment = require("moment");
const URI = require("urijs");
const fuzzy = require("fuzzy");

// our libraries
require("./libs/jquery/inputhistory");
require("./libs/jquery/stickyscroll");
const slideoutMenu = require("./libs/slideout");
const templates = require("../views");
const socket = require("./socket");
const render = require("./render");
require("./socket-events");
const storage = require("./localStorage");
const utils = require("./utils");
require("./webpush");
require("./keybinds");
require("./clipboard");
const Changelog = require("./socket-events/changelog");
const JoinChannel = require("./join-channel");

$(function() {
	const sidebar = $("#sidebar, #footer");
	const chat = $("#chat");

	$(document.body).data("app-name", document.title);

	const viewport = $("#viewport");
	const sidebarSlide = slideoutMenu(viewport[0], sidebar[0]);
	const contextMenuContainer = $("#context-menu-container");
	const contextMenu = $("#context-menu");

	$("#main").on("click", function(e) {
		if ($(e.target).is(".lt")) {
			sidebarSlide.toggle(!sidebarSlide.isOpen());
		} else if (sidebarSlide.isOpen()) {
			sidebarSlide.toggle(false);
		}
	});

	viewport.on("click", ".rt", function(e) {
		const self = $(this);
		viewport.toggleClass(self.prop("class"));
		e.stopPropagation();
		chat.find(".chan.active .chat").trigger("msg.sticky");
	});

	function positionContextMenu(that, e) {
		let offset;
		const menuWidth = contextMenu.outerWidth();
		const menuHeight = contextMenu.outerHeight();

		if (that.hasClass("menu")) {
			offset = that.offset();
			offset.left -= menuWidth - that.outerWidth();
			offset.top += that.outerHeight();
			return offset;
		}

		offset = {left: e.pageX, top: e.pageY};

		if ((window.innerWidth - offset.left) < menuWidth) {
			offset.left = window.innerWidth - menuWidth;
		}

		if ((window.innerHeight - offset.top) < menuHeight) {
			offset.top = window.innerHeight - menuHeight;
		}

		return offset;
	}

	function showContextMenu(that, e) {
		const target = $(e.currentTarget);
		let output = "";

		if (target.hasClass("user")) {
			output = templates.contextmenu_item({
				class: "user",
				action: "whois",
				text: target.text(),
				data: target.data("name"),
			});
			output += templates.contextmenu_divider();
			output += templates.contextmenu_item({
				class: "action-whois",
				action: "whois",
				text: "User information",
				data: target.data("name"),
			});
			output += templates.contextmenu_item({
				class: "action-query",
				action: "query",
				text: "Direct messages",
				data: target.data("name"),
			});

			const channel = target.closest(".chan");
			if (utils.isOpInChannel(channel) && channel.data("type") === "channel") {
				output += templates.contextmenu_divider();
				output += templates.contextmenu_item({
					class: "action-kick",
					action: "kick",
					text: "Kick",
					data: target.data("name"),
				});
			}
		} else if (target.hasClass("chan")) {
			let itemClass;

			if (target.hasClass("lobby")) {
				itemClass = "network";
			} else if (target.hasClass("query")) {
				itemClass = "query";
			} else {
				itemClass = "chan";
			}

			output = templates.contextmenu_item({
				class: itemClass,
				action: "focusChan",
				text: target.attr("aria-label"),
				data: target.data("target"),
			});
			output += templates.contextmenu_divider();
			if (target.hasClass("lobby")) {
				output += templates.contextmenu_item({
					class: "list",
					action: "list",
					text: "List all channels",
					data: target.data("id"),
				});
				output += templates.contextmenu_item({
					class: "join",
					action: "join",
					text: "Join a channel…",
					data: target.data("id"),
				});
			}
			if (target.hasClass("channel")) {
				output += templates.contextmenu_item({
					class: "list",
					action: "banlist",
					text: "List banned users",
					data: target.data("id"),
				});
			}
			output += templates.contextmenu_item({
				class: "close",
				action: "close",
				text: target.hasClass("lobby") ? "Disconnect" : target.hasClass("channel") ? "Leave" : "Close",
				data: target.data("target"),
			});
		}

		contextMenuContainer.show();
		contextMenu
			.html(output)
			.css(positionContextMenu($(that), e));

		return false;
	}

	viewport.on("contextmenu", ".network .chan", function(e) {
		return showContextMenu(this, e);
	});

	viewport.on("click contextmenu", ".user", function(e) {
		// If user is selecting text, do not open context menu
		// This primarily only targets mobile devices where selection is performed with touch
		if (!window.getSelection().isCollapsed) {
			return true;
		}

		return showContextMenu(this, e);
	});

	viewport.on("click", "#chat .menu", function(e) {
		e.currentTarget = $(`#sidebar .chan[data-id="${$(this).closest(".chan").data("id")}"]`)[0];
		return showContextMenu(this, e);
	});

	contextMenuContainer.on("click contextmenu", function() {
		contextMenuContainer.hide();
		return false;
	});

	function resetInputHeight(input) {
		input.style.height = input.style.minHeight;
	}

	const input = $("#input")
		.history()
		.on("input", function() {
			const style = window.getComputedStyle(this);

			// Start by resetting height before computing as scrollHeight does not
			// decrease when deleting characters
			resetInputHeight(this);

			this.style.height = Math.min(
				Math.round(window.innerHeight - 100), // prevent overflow
				this.scrollHeight
				+ Math.round(parseFloat(style.borderTopWidth) || 0)
				+ Math.round(parseFloat(style.borderBottomWidth) || 0)
			) + "px";

			chat.find(".chan.active .chat").trigger("msg.sticky"); // fix growing
		});

	let focus = $.noop;
	if (!("ontouchstart" in window || navigator.maxTouchPoints > 0)) {
		focus = function() {
			if (chat.find(".active").hasClass("chan")) {
				input.trigger("focus");
			}
		};

		$(window).on("focus", focus);

		chat.on("click", ".chat", function() {
			setTimeout(function() {
				if (window.getSelection().isCollapsed) {
					focus();
				}
			}, 2);
		});
	}

	if (navigator.platform.match(/(Mac|iPhone|iPod|iPad)/i)) {
		$(document.body).addClass("is-apple");
	}

	$("#form").on("submit", function(e) {
		e.preventDefault();
		utils.forceFocus();
		const text = input.val();

		if (text.length === 0) {
			return;
		}

		input.val("");
		resetInputHeight(input.get(0));

		if (text.charAt(0) === "/") {
			const args = text.substr(1).split(" ");
			const cmd = args.shift().toLowerCase();
			if (typeof utils.inputCommands[cmd] === "function" && utils.inputCommands[cmd](args)) {
				return;
			}
		}

		socket.emit("input", {
			target: chat.data("id"),
			text: text,
		});
	});

	$("button#set-nick").on("click", function() {
		utils.toggleNickEditor(true);

		// Selects existing nick in the editable text field
		const element = document.querySelector("#nick-value");
		element.focus();
		const range = document.createRange();
		range.selectNodeContents(element);
		const selection = window.getSelection();
		selection.removeAllRanges();
		selection.addRange(range);
	});

	$("button#cancel-nick").on("click", cancelNick);
	$("button#submit-nick").on("click", submitNick);

	function submitNick() {
		const newNick = $("#nick-value").text().trim();

		if (newNick.length === 0) {
			cancelNick();
			return;
		}

		utils.toggleNickEditor(false);

		socket.emit("input", {
			target: chat.data("id"),
			text: "/nick " + newNick,
		});
	}

	function cancelNick() {
		utils.setNick(sidebar.find(".chan.active").closest(".network").data("nick"));
	}

	$("#nick-value").on("keypress", function(e) {
		switch (e.keyCode ? e.keyCode : e.which) {
		case 13: // Enter
			// Ensures a new line is not added when pressing Enter
			e.preventDefault();
			break;
		}
	}).on("keyup", function(e) {
		switch (e.keyCode ? e.keyCode : e.which) {
		case 13: // Enter
			submitNick();
			break;
		case 27: // Escape
			cancelNick();
			break;
		}
	});

	chat.on("click", ".inline-channel", function() {
		const name = $(this).data("chan");
		const chan = utils.findCurrentNetworkChan(name);

		if (chan.length) {
			chan.trigger("click");
		} else {
			socket.emit("input", {
				target: chat.data("id"),
				text: "/join " + name,
			});
		}
	});

	chat.on("click", ".condensed-summary .content", function() {
		$(this).closest(".msg.condensed").toggleClass("closed");
	});

	const openWindow = function openWindow(e, data) {
		const self = $(this);
		const target = self.data("target");
		if (!target) {
			return;
		}

		// This is a rather gross hack to account for sources that are in the
		// sidebar specifically. Needs to be done better when window management gets
		// refactored.
		const inSidebar = self.parents("#sidebar, #footer").length > 0;

		if (inSidebar) {
			chat.data(
				"id",
				self.data("id")
			);
			socket.emit(
				"open",
				self.data("id")
			);

			sidebar.find(".active")
				.removeClass("active")
				.attr("aria-selected", false);

			self.addClass("active")
				.attr("aria-selected", true)
				.find(".badge")
				.removeClass("highlight")
				.empty();

			if (sidebar.find(".highlight").length === 0) {
				utils.toggleNotificationMarkers(false);
			}

			sidebarSlide.toggle(false);
		}

		const lastActive = $("#windows > .active");

		lastActive
			.removeClass("active")
			.find(".chat")
			.unsticky();

		const lastActiveChan = lastActive.find(".chan.active");

		if (lastActiveChan.length > 0) {
			lastActiveChan
				.removeClass("active")
				.find(".unread-marker")
				.data("unread-id", 0)
				.appendTo(lastActiveChan.find(".messages"));

			render.trimMessageInChannel(lastActiveChan, 100);
		}

		const chan = $(target)
			.addClass("active")
			.trigger("show");

		let title = $(document.body).data("app-name");
		const chanTitle = chan.attr("aria-label");
		if (chanTitle.length > 0) {
			title = `${chanTitle} — ${title}`;
		}
		document.title = title;

		const type = chan.data("type");
		let placeholder = "";
		if (type === "channel" || type === "query") {
			placeholder = `Write to ${chanTitle}`;
		}

		input
			.prop("placeholder", placeholder)
			.attr("aria-label", placeholder);

		if (self.hasClass("chan")) {
			$("#chat-container").addClass("active");
			utils.setNick(self.closest(".network").data("nick"));
		}

		const chanChat = chan.find(".chat");
		if (chanChat.length > 0 && type !== "special") {
			chanChat.sticky();
		}

		if (chan.data("needsNamesRefresh") === true) {
			chan.data("needsNamesRefresh", false);
			socket.emit("names", {target: self.data("id")});
		}

		if (target === "#settings") {
			$("#session-list").html("<p>Loading…</p>");
			socket.emit("sessions:get");
		}

		if (target === "#help" || target === "#changelog") {
			Changelog.requestIfNeeded();
		}

		focus();

		// Pushes states to history web API when clicking elements with a data-target attribute.
		// States are very trivial and only contain a single `clickTarget` property which
		// contains a CSS selector that targets elements which takes the user to a different view
		// when clicked. The `popstate` event listener will trigger synthetic click events using that
		// selector and thus take the user to a different view/state.
		if (data && data.pushState === false) {
			return;
		}
		const state = {};

		if (self.prop("id")) {
			state.clickTarget = `#${self.prop("id")}`;
		} else if (self.hasClass("chan")) {
			state.clickTarget = `#sidebar .chan[data-id="${self.data("id")}"]`;
		} else {
			state.clickTarget = `#footer button[data-target="${target}"]`;
		}

		if (history && history.pushState) {
			if (data && data.replaceHistory && history.replaceState) {
				history.replaceState(state, null, target);
			} else {
				history.pushState(state, null, target);
			}
		}

		return false;
	};

	sidebar.on("click", ".chan, button", openWindow);
	$("#help").on("click", "#view-changelog, #back-to-help", openWindow);
	$("#changelog").on("click", "#back-to-help", openWindow);

	sidebar.on("click", "#sign-out", function() {
		socket.emit("sign-out");
		storage.remove("token");

		if (!socket.connected) {
			location.reload();
		}
	});

	function closeChan(chan) {
		let cmd = "/close";

		if (chan.hasClass("lobby")) {
			cmd = "/quit";
			const server = chan.find(".name").html();
			if (!confirm("Disconnect from " + server + "?")) { // eslint-disable-line no-alert
				return false;
			}
		}
		socket.emit("input", {
			target: chan.data("id"),
			text: cmd,
		});
		chan.css({
			transition: "none",
			opacity: 0.4,
		});
		return false;
	}

	sidebar.on("click", ".close", function() {
		closeChan($(this).closest(".chan"));
	});

	const contextMenuActions = {
		join: function(itemData) {
			const network = $(`#join-channel-${itemData}`).closest(".network");
			JoinChannel.openForm(network);
		},
		close: function(itemData) {
			closeChan($(`.networks .chan[data-target="${itemData}"]`));
		},
		focusChan: function(itemData) {
			$(`.networks .chan[data-target="${itemData}"]`).trigger("click");
		},
		list: function(itemData) {
			socket.emit("input", {
				target: itemData,
				text: "/list",
			});
		},
		banlist: function(itemData) {
			socket.emit("input", {
				target: itemData,
				text: "/banlist",
			});
		},
		whois: function(itemData) {
			const chan = utils.findCurrentNetworkChan(itemData);

			if (chan.length) {
				chan.trigger("click");
			}

			socket.emit("input", {
				target: $("#chat").data("id"),
				text: "/whois " + itemData,
			});

			$(`.channel.active .users .user[data-name="${itemData}"]`).trigger("click");
		},
		query: function(itemData) {
			const chan = utils.findCurrentNetworkChan(itemData);

			if (chan.length) {
				chan.trigger("click");
			}

			socket.emit("input", {
				target: $("#chat").data("id"),
				text: "/query " + itemData,
			});
		},
		kick: function(itemData) {
			socket.emit("input", {
				target: $("#chat").data("id"),
				text: "/kick " + itemData,
			});
		},
	};

	contextMenuActions.execute = (name, ...args) => contextMenuActions[name] && contextMenuActions[name](...args);

	contextMenu.on("click", ".context-menu-item", function() {
		const $this = $(this);
		const itemData = $this.data("data");
		const contextAction = $this.data("action");
		contextMenuActions.execute(contextAction, itemData);
	});

	chat.on("input", ".search", function() {
		const value = $(this).val();
		const parent = $(this).closest(".users");
		const names = parent.find(".names-original");
		const container = parent.find(".names-filtered");

		if (!value.length) {
			container.hide();
			names.show();
			return;
		}

		const fuzzyOptions = {
			pre: "<b>",
			post: "</b>",
			extract: (el) => $(el).text(),
		};

		const result = fuzzy.filter(
			value,
			names.find(".user").toArray(),
			fuzzyOptions
		);

		names.hide();
		container.html(templates.user_filtered({matches: result})).show();
	});

	if ($("body").hasClass("public") && (window.location.hash === "#connect" || window.location.hash === "")) {
		$("#connect").one("show", function() {
			const params = URI(document.location.search).search(true);
			// Possible parameters:  name, host, port, password, tls, nick, username, realname, join
			// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for...in#Iterating_over_own_properties_only
			for (let key in params) {
				if (params.hasOwnProperty(key)) {
					const value = params[key];
					// \W searches for non-word characters
					key = key.replace(/\W/g, "");

					const element = $("#connect input[name='" + key + "']");
					// if the element exists, it isn't disabled, and it isn't hidden
					if (element.length > 0 && !element.is(":disabled") && !element.is(":hidden")) {
						if (element.is(":checkbox")) {
							element.prop("checked", (value === "1" || value === "true") ? true : false);
						} else {
							element.val(value);
						}
					}
				}
			}
		});
	}

	$(document).on("visibilitychange focus click", () => {
		if (sidebar.find(".highlight").length === 0) {
			utils.toggleNotificationMarkers(false);
		}
	});

	// Compute how many milliseconds are remaining until the next day starts
	function msUntilNextDay() {
		return moment().add(1, "day").startOf("day") - moment();
	}

	// Go through all Today/Yesterday date markers in the DOM and recompute their
	// labels. When done, restart the timer for the next day.
	function updateDateMarkers() {
		$(".date-marker-text[data-label='Today'], .date-marker-text[data-label='Yesterday']")
			.closest(".date-marker-container")
			.each(function() {
				$(this).replaceWith(templates.date_marker({time: $(this).data("time")}));
			});

		// This should always be 24h later but re-computing exact value just in case
		setTimeout(updateDateMarkers, msUntilNextDay());
	}
	setTimeout(updateDateMarkers, msUntilNextDay());

	window.addEventListener("popstate", (e) => {
		const {state} = e;
		if (!state) {
			return;
		}

		let {clickTarget} = state;

		if (clickTarget) {
			// This will be true when click target corresponds to opening a thumbnail,
			// browsing to the previous/next thumbnail, or closing the image viewer.
			const imageViewerRelated = clickTarget.includes(".toggle-thumbnail");

			// If the click target is not related to the image viewer but the viewer
			// is currently opened, we need to close it.
			if (!imageViewerRelated && $("#image-viewer").hasClass("opened")) {
				clickTarget += ", #image-viewer";
			}

			// Emit the click to the target, while making sure it is not going to be
			// added to the state again.
			$(clickTarget).trigger("click", {
				pushState: false,
			});
		}
	});

	// Only start opening socket.io connection after all events have been registered
	socket.open();
});