import {Localuser} from "./localuser.js";
import {Channel} from "./channel.js";
import {Emoji} from "./emoji.js";
import {Guild} from "./guild.js";
import {I18n} from "./i18n.js";
import {Dialog} from "./settings.js";
import {Contextmenu} from "./contextmenu.js";
import {highlight} from "./highlighter/index.js";
import {TypeBox} from "./typeBox.js";
const linkMenu = new Contextmenu<string, void>("copyLink", true);
linkMenu.addButton(
	() => I18n.copyRegLink(),
	function () {
		navigator.clipboard.writeText(this);
	},
	{group: "copyLink"},
);
const isFirefox = Error.prototype.stack === "";
class MarkDown {
	static emoji?: typeof Emoji;
	txt: string[];
	keep: boolean;
	stdsize: boolean;
	owner: Localuser | Channel | void;
	info: Localuser["info"] | void = undefined;
	constructor(
		text: string | string[],
		owner: MarkDown["owner"],
		{keep = false, stdsize = false} = {},
	) {
		if (typeof text === typeof "") {
			this.txt = (text as string).split("");
		} else {
			this.txt = text as string[];
		}
		if (this.txt === undefined) {
			this.txt = [];
		}
		if (owner) {
			this.info = owner.info;
		}
		this.keep = keep;
		this.owner = owner;
		this.stdsize = stdsize;
	}
	get channel() {
		if (!this.owner) return;
		if ("user" in this.owner) {
			return;
		} else if (this.owner) {
			return this.owner;
		}
		return null;
	}
	get localuser() {
		if (!this.owner) return;
		if ("user" in this.owner) {
			return this.owner;
		} else if (this.owner) {
			return this.owner.localuser;
		}
		return null;
	}
	get rawString() {
		return this.txt.join("");
	}
	get textContent() {
		return this.makeHTML().textContent;
	}
	static getText() {
		return text;
	}
	makeHTML({keep = this.keep, stdsize = this.stdsize} = {}) {
		return this.markdown(this.txt, {keep, stdsize});
	}
	markdown(text: string | string[], {keep = false, stdsize = false} = {}) {
		if (!keep && !stdsize) {
			let str: string;
			if (text instanceof Array) {
				str = text.join("");
			} else {
				str = text;
			}
			const span = document.createElement("span");
			span.classList.add("md-emoji", "bigemojiUni");
			const seg = new Intl.Segmenter("en-US", {granularity: "grapheme"});
			const matchReg = !!str.match(/^(((<a?:[A-Za-z\d_]*:\d*>|([^\da-zA-Z <>]+)) *))$/gu);
			const matched = matchReg
				? Array.from(str.matchAll(/((<a?:[A-Za-z\d_]*:\d*>|([^\da-zA-Z <>]+)) *)/gu) || []).flatMap(
						(match) => {
							if (match[0].match(/^<a?:[A-Za-z\d_]*:\d*>$/u)) {
								return match[0];
							} else if (match[0].match(/[^\da-zA-Z <>]+$/gu)) {
								return seg.segment(match[0]);
							} else if (match[0].match(/^\s*$/)) {
								console.log(match);
								return [];
							} else return [..."aaaa"];
						},
					)
				: [..."aaaa"];
			if (matched.length <= 3) {
				const map = [...str.matchAll(/<a?:[A-Za-z\d_]*:\d*>|[^\da-zA-Z <>]+/gu).map(([_]) => _)];

				const invalid = map.find((str) => {
					if (str.length > 10) return false;
					if (Array.from(seg.segment(str)).length !== 1) return true;
					return false;
				});
				if (!invalid) {
					for (const match of map) {
						if (match.length > 10) {
							const parts = match.match(/^<(a)?:\w+:(\d{10,30})>$/);
							if (parts && parts[2]) {
								const owner = this.channel ? this.channel.guild : this.localuser;
								if (!owner) continue;
								const emoji = new Emoji(
									{name: match, id: parts[2], animated: Boolean(parts[1])},
									owner,
								);
								span.appendChild(emoji.getHTML(true, !keep));

								continue;
							}
						} else {
							span.append(match);
						}
					}
					return span;
				}
			}
		}
		let txt: string[];
		if (typeof text === typeof "") {
			txt = (text as string).split("");
		} else {
			txt = text as string[];
		}
		if (txt === undefined) {
			txt = [];
		}
		const span = document.createElement("span");
		let current = document.createElement("span");
		function appendcurrent() {
			if (current.innerHTML !== "") {
				span.append(current);
				current = document.createElement("span");
			}
		}
		function getCurLast(): Node | undefined {
			return Array.from(span.childNodes).at(-1);
		}
		for (let i = 0; i < txt.length; i++) {
			const isSpace = /^\s$/;
			if (txt[i] === "\n" || i === 0) {
				let first = i === 0;
				if (first) {
					i--;
				}
				let element: HTMLElement = document.createElement("span");
				let keepys = "";

				if (txt[i + 1] === "#") {
					if (txt[i + 2] === "#") {
						if (txt[i + 3] === "#" && txt[i + 4]?.match(isSpace)) {
							element = document.createElement("h3");
							keepys = "### ";
							i += 5;
						} else if (txt[i + 3]?.match(isSpace)) {
							element = document.createElement("h2");
							element.classList.add("h2md");
							keepys = "## ";
							i += 4;
						}
					} else if (txt[i + 2]?.match(isSpace)) {
						element = document.createElement("h1");
						keepys = "# ";
						i += 3;
					}
				} else if (txt[i + 1] === ">" && txt[i + 2]?.match(isSpace)) {
					element = document.createElement("div");
					const line = document.createElement("div");
					line.classList.add("quoteline");
					element.append(line);
					element.classList.add("quote");
					keepys = "> ";
					i += 3;
				} else if (txt[i + 1] === "-" && txt[i + 2] === "#" && txt[i + 3]?.match(isSpace)) {
					element = document.createElement("small");
					keepys = "-# ";
					i += 4;
				}
				if (keepys) {
					appendcurrent();
					if (!first && !stdsize) {
						span.appendChild(document.createElement("br"));
					}
					const build: string[] = [];
					for (; txt[i] !== "\n" && txt[i] !== undefined; i++) {
						build.push(txt[i]);
					}
					try {
						if (stdsize) {
							element = document.createElement("span");
						}
						if (keep) {
							element.append(keepys);
						}
						element.appendChild(this.markdown(build, {keep, stdsize}));
						span.append(element);
					} finally {
						i -= 1;
						continue;
					}
				}
				const bullet = new Set("*+- ");
				if (bullet.has(txt[i + 1])) {
					let list = document.createElement("ul");
					let depth = 0;
					while (true) {
						let j = i + 1;
						let build = "";
						for (; txt[j] === " "; j++) {
							build += txt[j];
						}
						build += txt[j];
						j++;
						build += txt[j];
						j++;
						const match = build.match(/( *)[+*-] $/);
						if (match) {
							const arr: string[] = [];
							for (; txt[j] && txt[j] !== "\n"; j++) {
								arr.push(txt[j]);
							}
							i = j;
							const line = this.markdown(arr);
							if (keep) {
								if (!first) {
									current.textContent += "\n";
								} else {
									first = false;
								}
								current.textContent += build;
								appendcurrent();
								span.append(line);
								depth = 2;
							} else {
								const curDepth = 0 | (match[1].length / 2);
								if (curDepth > depth) {
									depth++;
									const newlist = document.createElement("ul");
									list.append(newlist);
									list = newlist;
								} else {
									while (curDepth < depth) {
										depth--;
										list = list.parentElement as HTMLUListElement;
									}
								}
								const li = document.createElement("li");
								li.append(line);
								list.append(li);
							}
						} else {
							break;
						}
						if (!txt[j]) {
							break;
						}
					}
					if (depth !== 0 || list.children.length) {
						if (!keep) {
							while (0 < depth) {
								depth--;
								list = list.parentElement as HTMLUListElement;
							}
							appendcurrent();
							span.append(list);
						}
						i--;
						continue;
					}
				}
				if (first) {
					i++;
				}
			}
			if (txt[i] === "\\") {
				const chatset = new Set("\\`{}[]()<>*_#+-.!|@".split(""));
				if (chatset.has(txt[i + 1])) {
					if (keep) {
						current.textContent += txt[i];
					}
					current.textContent += txt[i + 1];
					i++;
					continue;
				}
			}
			if (txt[i] === "\n") {
				if (!stdsize) {
					const last = getCurLast();
					if (last instanceof HTMLElement && last.contentEditable === "false") {
						span.append(document.createElement("span"));
					}
					appendcurrent();
					if (keep) {
						span.append(new Text("\n"));
					} else {
						span.append(document.createElement("br"));
					}
				}
				continue;
			}
			if (txt[i] === "`") {
				let count = 1;
				if (txt[i + 1] === "`") {
					count++;
					if (txt[i + 2] === "`") {
						count++;
					}
				}
				let build = "";
				if (keep) {
					build += "`".repeat(count);
				}
				let find = 0;
				let j = i + count;
				let init = true;
				let lang = "";
				for (; txt[j] !== undefined && (txt[j] !== "\n" || count === 3) && find !== count; j++) {
					if (txt[j] === "`") {
						find++;
					} else {
						if (find !== 0) {
							build += "`".repeat(find);
							find = 0;
						}
						if (init && count === 3) {
							if (txt[j] === " " || txt[j] === "\n") {
								init = false;
							}
							lang += txt[j];
							if (keep) {
								build += txt[j];
							}
							continue;
						}
						build += txt[j];
					}
				}
				if (stdsize) {
					build = build.replaceAll("\n", "");
				}
				if (find === count || (keep && count === 3)) {
					appendcurrent();
					i = j;
					if (keep) {
						build += "`".repeat(find);
					}
					if (build.endsWith("\n")) build += "\n";
					if (count !== 3 && !stdsize) {
						const samp = document.createElement("samp");
						samp.textContent = build;
						span.appendChild(samp);
					} else {
						const pre = document.createElement("pre");
						if (build.at(-1) === "\n" && !isFirefox) {
							build = build.substring(0, build.length - 1);
						}
						if (txt[i] === "\n") {
							i++;
						}
						pre.textContent = build;
						span.appendChild(pre);
						if (count === 3) highlight(pre, lang, keep ? 3 + lang.length : 0, keep ? 3 : 0);
						if (!keep && !stdsize && lang && count === 3) {
							const button = document.createElement("button");
							button.textContent = I18n.copyCode();
							button.classList.add("copyCode");
							pre.append(button);
							button.onclick = () => {
								navigator.clipboard.writeText(build);
							};
						}
					}
					i--;
					continue;
				}
			}

			if (txt[i] === "*") {
				let count = 1;
				if (txt[i + 1] === "*") {
					count++;
					if (txt[i + 2] === "*") {
						count++;
					}
				}
				let build: string[] = [];
				let find = 0;
				let j = i + count;
				for (; txt[j] !== undefined && find !== count; j++) {
					if (txt[j] === "*") {
						find++;
					} else {
						build.push(txt[j]);
						if (find !== 0) {
							build = build.concat(new Array(find).fill("*"));
							find = 0;
						}
					}
				}
				if (find === count && (count != 1 || txt[i + 1] !== " ")) {
					appendcurrent();
					i = j;

					const stars = "*".repeat(count);
					if (count === 1) {
						const i = document.createElement("i");
						if (keep) {
							i.append(stars);
						}
						i.appendChild(this.markdown(build, {keep, stdsize}));
						if (keep) {
							i.append(stars);
						}
						span.appendChild(i);
					} else if (count === 2) {
						const b = document.createElement("b");
						if (keep) {
							b.append(stars);
						}
						b.appendChild(this.markdown(build, {keep, stdsize}));
						if (keep) {
							b.append(stars);
						}
						span.appendChild(b);
					} else {
						const b = document.createElement("b");
						const i = document.createElement("i");
						if (keep) {
							b.append(stars);
						}
						b.appendChild(this.markdown(build, {keep, stdsize}));
						if (keep) {
							b.append(stars);
						}
						i.appendChild(b);
						span.appendChild(i);
					}
					i--;
					continue;
				}
			}

			if (txt[i] === "_") {
				let count = 1;
				if (txt[i + 1] === "_") {
					count++;
					if (txt[i + 2] === "_") {
						count++;
					}
				}
				let build: string[] = [];
				let find = 0;
				let j = i + count;
				for (; txt[j] !== undefined && find !== count; j++) {
					if (txt[j] === "_") {
						find++;
					} else {
						build.push(txt[j]);
						if (find !== 0) {
							build = build.concat(new Array(find).fill("_"));
							find = 0;
						}
					}
				}
				if (
					find === count &&
					(count != 1 || txt[j] === " " || txt[j] === "\n" || txt[j] === undefined)
				) {
					appendcurrent();
					i = j;
					const underscores = "_".repeat(count);
					if (count === 1) {
						const i = document.createElement("i");
						if (keep) {
							i.append(underscores);
						}
						i.appendChild(this.markdown(build, {keep, stdsize}));
						if (keep) {
							i.append(underscores);
						}
						span.appendChild(i);
					} else if (count === 2) {
						const u = document.createElement("u");
						if (keep) {
							u.append(underscores);
						}
						u.appendChild(this.markdown(build, {keep, stdsize}));
						if (keep) {
							u.append(underscores);
						}
						span.appendChild(u);
					} else {
						const u = document.createElement("u");
						const i = document.createElement("i");
						if (keep) {
							i.append(underscores);
						}
						i.appendChild(this.markdown(build, {keep, stdsize}));
						if (keep) {
							i.append(underscores);
						}
						u.appendChild(i);
						span.appendChild(u);
					}
					i--;
					continue;
				}
			}

			if (txt[i] === "~" && txt[i + 1] === "~") {
				const count = 2;
				let build: string[] = [];
				let find = 0;
				let j = i + 2;
				for (; txt[j] !== undefined && find !== count; j++) {
					if (txt[j] === "~") {
						find++;
					} else {
						build.push(txt[j]);
						if (find !== 0) {
							build = build.concat(new Array(find).fill("~"));
							find = 0;
						}
					}
				}
				if (find === count) {
					appendcurrent();
					i = j - 1;
					const tildes = "~~";
					if (count === 2) {
						const s = document.createElement("s");
						if (keep) {
							s.append(tildes);
						}
						s.appendChild(this.markdown(build, {keep, stdsize}));
						if (keep) {
							s.append(tildes);
						}
						span.appendChild(s);
					}
					continue;
				}
			}
			if (txt[i] === "|" && txt[i + 1] === "|") {
				const count = 2;
				let build: string[] = [];
				let find = 0;
				let j = i + 2;
				for (; txt[j] !== undefined && find !== count; j++) {
					if (txt[j] === "|") {
						find++;
					} else {
						build.push(txt[j]);
						if (find !== 0) {
							build = build.concat(new Array(find).fill("~"));
							find = 0;
						}
					}
				}
				if (find === count) {
					appendcurrent();
					i = j - 1;
					const pipes = "||";
					if (count === 2) {
						const j = document.createElement("j");
						if (keep) {
							j.append(pipes);
						}
						j.appendChild(this.markdown(build, {keep, stdsize}));
						j.classList.add("spoiler");
						j.onclick = MarkDown.unspoil;
						if (keep) {
							j.click();
						}
						if (keep) {
							j.append(pipes);
						}
						span.appendChild(j);
					}
					continue;
				}
			}
			if (
				!keep &&
				txt[i] === "h" &&
				txt[i + 1] === "t" &&
				txt[i + 2] === "t" &&
				txt[i + 3] === "p"
			) {
				let build = "http";
				let j = i + 4;
				const endchars = new Set("\\<>|[] \n(){}");
				for (; txt[j] !== undefined; j++) {
					const char = txt[j];
					if (endchars.has(char)) {
						break;
					}
					build += char;
				}
				if (URL.canParse(build)) {
					appendcurrent();
					const a = document.createElement("a");
					linkMenu.bindContextmenu(a, build);
					//a.href=build;

					a.textContent = build;
					if (!stdsize) {
						const replace = MarkDown.safeLink(a, build, this.localuser);
						if (replace) {
							a.textContent = replace;
							a.classList.add("mentionMD");
						}
						a.target = "_blank";
					}

					i = j - 1;
					span.appendChild(a);
					continue;
				}
			}
			if (txt[i] === "@") {
				let j = i + 1;
				let everyone = true;
				for (const char of "everyone") {
					if (char !== txt[j]) {
						everyone = false;
						break;
					}
					j++;
				}
				let here = false;
				if (!everyone) {
					here = true;
					for (const char of "here") {
						if (char !== txt[j]) {
							here = false;
							break;
						}
						j++;
					}
				}
				if (everyone || here) {
					i = j - 1;
					const mention = document.createElement("span");
					mention.classList.add("mentionMD");
					mention.contentEditable = "false";
					mention.textContent = everyone ? "@everyone" : "@here";
					appendcurrent();
					span.appendChild(mention);
					mention.setAttribute("real", everyone ? `@everyone` : "@here");
					continue;
				}
			}
			if (txt[i] === "<") {
				if ((txt[i + 1] === "@" || txt[i + 1] === "#") && this.localuser) {
					let id = "";
					const role = txt[i + 1] === "@" && txt[i + 2] === "&";
					let j = i + 2 + +role;
					const numbers = new Set(["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"]);
					for (; txt[j] !== undefined; j++) {
						const char = txt[j];
						if (!numbers.has(char)) {
							break;
						}
						id += char;
					}

					if (txt[j] === ">") {
						appendcurrent();
						const last = getCurLast();
						if (
							last instanceof HTMLBRElement ||
							(last instanceof HTMLElement && last.contentEditable === "false")
						) {
							span.append(document.createElement("span"));
						}
						const mention = document.createElement("span");
						mention.classList.add("mentionMD");
						mention.contentEditable = "false";
						const char = txt[i + 1];
						i = j;
						switch (char) {
							case "@":
								if (role) {
									if (this.channel) {
										const role = this.channel.guild.roleids.get(id);
										if (role) {
											mention.textContent = `@${role.name}`;
											mention.style.color = `var(--role-${role.id})`;
										} else {
											mention.textContent = I18n.guild.unknownRole();
										}
									}
								} else {
									(async () => {
										mention.textContent = I18n.userping.resolving();
										const user = await this.localuser?.getUser(id);
										if (user) {
											mention.textContent = `@${user.name}`;
											let guild: null | Guild = null;
											if (this.channel) {
												guild = this.channel.guild;
											}
											if (!keep) {
												user.bind(mention, guild, true, stdsize ? "none" : "left");
											}
											if (guild) {
												guild.resolveMember(user).then((member) => {
													if (member) {
														mention.textContent = `@${member.name}`;
													}
												});
											}
										} else {
											mention.textContent = I18n.userping.unknown();
										}
									})();
								}
								break;
							case "#":
								const channel = this.localuser.channels.get(id);
								if (channel) {
									mention.textContent = `#${channel.name}`;
									if (!keep && !stdsize) {
										mention.onclick = (_) => {
											if (!this.localuser) return;
											this.localuser.goToChannel(id);
										};
									}
								} else {
									mention.textContent = "#unknown";
								}
								break;
						}
						span.appendChild(mention);
						mention.setAttribute("real", `<${char}${role ? "&" : ""}${id}>`);
						continue;
					}
				} else {
					let j = i + 1;
					let build = "";
					const invalid = new Set([">", "<"]);
					for (; txt[j] !== undefined; j++) {
						const char = txt[j];
						if (invalid.has(char)) {
							break;
						}
						build += char;
					}
					if (URL.canParse(build) && txt[j] === ">") {
						const url = new URL(build);
						const allowedprotocols = new Set(["https:", "http:"]);
						if (allowedprotocols.has(url.protocol)) {
							i = j;

							if (keep) {
								current.textContent += `<${build}>`;
							} else {
								appendcurrent();
								const a = document.createElement("a");
								linkMenu.bindContextmenu(a, build);
								if (!stdsize) {
									const text = MarkDown.safeLink(a, build, this.localuser);
									if (text) {
										a.textContent = text;
										a.classList.add("mentionMD");
									} else {
										a.textContent = build;
									}
									a.target = "_blank";
								}
								span.appendChild(a);
							}
							continue;
						}
					}
				}
			}
			if (txt[i] === "<" && txt[i + 1] === "t" && txt[i + 2] === ":") {
				let found = false;
				const build = ["<", "t", ":"];
				let j = i + 3;
				for (; txt[j] !== void 0; j++) {
					build.push(txt[j]);

					if (txt[j] === ">") {
						found = true;
						break;
					}
				}
				const parts = build.join("").match(/^<t:([0-9]{1,16})(:([tTdDfFRS]))?>$/);

				if (found && parts) {
					appendcurrent();
					i = j;

					const dateInput = new Date(Number.parseInt(parts[1]) * 1000);
					let time = "";
					if (Number.isNaN(dateInput.getTime())) time = build.join("");
					else {
						if (parts[3] === "d")
							time = dateInput.toLocaleString(void 0, {
								day: "2-digit",
								month: "2-digit",
								year: "numeric",
							});
						else if (parts[3] === "D")
							time = dateInput.toLocaleString(void 0, {
								day: "numeric",
								month: "long",
								year: "numeric",
							});
						else if (!parts[3] || parts[3] === "f")
							time =
								dateInput.toLocaleString(void 0, {
									day: "numeric",
									month: "long",
									year: "numeric",
								}) +
								" " +
								dateInput.toLocaleString(void 0, {
									hour: "2-digit",
									minute: "2-digit",
								});
						else if (parts[3] === "F")
							time =
								dateInput.toLocaleString(void 0, {
									day: "numeric",
									month: "long",
									year: "numeric",
									weekday: "long",
								}) +
								" " +
								dateInput.toLocaleString(void 0, {
									hour: "2-digit",
									minute: "2-digit",
								});
						else if (parts[3] === "t")
							time = dateInput.toLocaleString(void 0, {
								hour: "2-digit",
								minute: "2-digit",
							});
						else if (parts[3] === "T")
							time = dateInput.toLocaleString(void 0, {
								hour: "2-digit",
								minute: "2-digit",
								second: "2-digit",
							});
						else if (parts[3] === "R")
							//TODO make this a little less bad
							time = MarkDown.relTime(new Date(Number.parseInt(parts[1]) * 1000));
						else if (parts[3] === "S")
							time =
								dateInput.toLocaleString(void 0, {
									day: "numeric",
									month: "numeric",
									year: "numeric",
								}) +
								" " +
								dateInput.toLocaleString(void 0, {
									hour: "2-digit",
									minute: "2-digit",
									second: "2-digit",
								});
					}

					const timeElem = document.createElement("span");
					timeElem.setAttribute("real", build.join(""));
					timeElem.contentEditable = "false";
					timeElem.classList.add("markdown-timestamp");
					timeElem.textContent = time;
					span.appendChild(timeElem);
					continue;
				}
			}

			if (
				txt[i] === "<" &&
				(txt[i + 1] === ":" || (txt[i + 1] === "a" && txt[i + 2] === ":" && this.owner))
			) {
				const Emoji = MarkDown.emoji;
				let found = false;
				const build = txt[i + 1] === "a" ? ["<", "a", ":"] : ["<", ":"];
				let j = i + build.length;
				for (; txt[j] !== void 0; j++) {
					build.push(txt[j]);

					if (txt[j] === ">") {
						found = true;
						break;
					}
				}

				if (found && Emoji) {
					const buildjoin = build.join("");
					const parts = buildjoin.match(/^<(a)?:\w+:(\d{10,30})>$/);
					if (parts && parts[2]) {
						appendcurrent();
						i = j;
						const isEmojiOnly = txt.join("").trim() === buildjoin.trim() && !stdsize;
						const owner = this.channel ? this.channel.guild : this.localuser;
						if (!owner) continue;
						const emoji = new Emoji(
							{name: buildjoin, id: parts[2], animated: Boolean(parts[1])},
							owner,
						);
						span.appendChild(emoji.getHTML(isEmojiOnly, !keep));

						continue;
					}
				}
			}

			if (txt[i] == "[" && !keep) {
				let partsFound = 0;
				let j = i + 1;
				const build = ["["];
				for (; txt[j] !== void 0; j++) {
					build.push(txt[j]);

					if (partsFound === 0 && txt[j] === "]") {
						if (
							(txt[j + 1] === "(" &&
								txt[j + 2] === "h" &&
								txt[j + 3] === "t" &&
								txt[j + 4] === "t" &&
								txt[j + 5] === "p" &&
								(txt[j + 6] === "s" || txt[j + 6] === ":")) ||
							(txt[j + 1] === "(" &&
								txt[j + 2] === "<" &&
								txt[j + 3] === "h" &&
								txt[j + 4] === "t" &&
								txt[j + 5] === "t" &&
								txt[j + 6] === "p" &&
								(txt[j + 7] === "s" || txt[j + 7] === ":"))
						) {
							partsFound++;
						} else {
							break;
						}
					} else if (partsFound === 1 && txt[j] === ")") {
						partsFound++;
						break;
					}
				}
				if (partsFound === 2) {
					appendcurrent();

					const parts = build.join("").match(/^\[(.+)\]\(<?(https?:.+?)>?( ('|").+('|"))?\)$/);
					if (parts) {
						const linkElem = document.createElement("a");

						if (URL.canParse(parts[2])) {
							i = j;
							if (!stdsize) {
								MarkDown.safeLink(linkElem, parts[2]);
								linkElem.append(this.markdown(parts[1], {keep, stdsize}));
								linkElem.target = "_blank";
								linkElem.rel = "noopener noreferrer";
							}
							linkElem.title =
								(parts[3] ? parts[3].substring(2, parts[3].length - 1) + "\n\n" : "") + parts[2];
							span.appendChild(linkElem);

							continue;
						}
					}
				}
			}
			current.textContent += txt[i];
		}
		appendcurrent();
		const last = getCurLast();
		function addSpacer() {
			const s = document.createElement("span");
			s.style.setProperty("white-space", "pre");
			s.textContent = "​";
			s.setAttribute("real", "");
			span.append(s);
		}
		if (
			last &&
			isFirefox &&
			(last instanceof Text || last instanceof HTMLSpanElement) &&
			last.textContent === "\n"
		) {
			addSpacer();
			span.append(current);
		}
		if (
			last &&
			last instanceof HTMLElement &&
			(last.contentEditable === "false" || last instanceof HTMLBRElement)
		) {
			span.append(current);
		}
		return span;
	}
	static relTime(date: Date, nextUpdate?: () => void): string {
		const r = Date.now() - +date;
		if (isNaN(r)) return "NaN";
		const up = r < 0;
		const time = Math.abs(r);

		let seconds = Math.round(time / 1000);
		const round = time % 1000;
		let minutes = Math.floor(seconds / 60);
		seconds -= minutes * 60;
		let hours = Math.floor(minutes / 60);
		minutes -= hours * 60;
		let days = Math.floor(hours / 24);
		hours -= days * 24;
		let years = Math.floor(days / 24);
		days -= years * 365;

		const formatter = new Intl.RelativeTimeFormat(I18n.lang, {style: "short"});
		if (years) {
			if (nextUpdate) {
				const ti = round + (seconds + (minutes + (hours + days * 24) * 60) * 60) * 1000;
				setTimeout(nextUpdate, up ? 1000 * 60 * 60 * 24 * 365 - ti : ti);
			}
			return formatter.format(up ? years : -years, "year");
		} else if (days) {
			if (nextUpdate) {
				const ti = round + (seconds + (minutes + hours * 60) * 60) * 1000;
				setTimeout(nextUpdate, up ? 1000 * 60 * 60 * 24 - ti : ti);
			}
			return formatter.format(up ? days : -days, "days");
		} else if (hours) {
			if (nextUpdate) {
				const ti = round + (seconds + minutes * 60) * 1000;
				setTimeout(nextUpdate, up ? 1000 * 60 * 60 - ti : ti);
			}
			return formatter.format(up ? hours : -hours, "hours");
		} else if (minutes) {
			if (nextUpdate) {
				const ti = round + seconds * 1000;
				setTimeout(nextUpdate, up ? 1000 * 60 - ti : ti);
			}
			return formatter.format(up ? minutes : -minutes, "minutes");
		} else {
			if (nextUpdate) setTimeout(nextUpdate, up ? 1000 - round : round);
			return formatter.format(up ? seconds : -seconds, "seconds");
		}
	}
	static unspoil(e: any): void {
		e.currentTarget.classList.remove("spoiler");
		e.currentTarget.classList.add("unspoiled");
	}
	onUpdate: (upto: string, pre: boolean) => unknown = () => {};
	box = new WeakRef(document.createElement("div"));
	giveBox(box: HTMLDivElement, onUpdate: (upto: string, pre: boolean) => unknown = () => {}) {
		this.box = new WeakRef(box);
		this.onUpdate = onUpdate;
		box.addEventListener("keydown", (_) => {
			if (_.isComposing) return;
			if (Error.prototype.stack !== "") return;
		});
		let prevcontent = "";
		const gatherBoxContents = (isBackSpace: boolean) => {
			let content = MarkDown.gatherBoxText(box);
			if (content === "\n") content = "";
			if (content !== prevcontent) {
				prevcontent = content;
				this.txt = content.split("");
				this.boxupdate(undefined, undefined, undefined, isBackSpace);
				MarkDown.gatherBoxText(box);
			}
		};
		const insertText = (txt: string, trim = false) => {
			const selection = window.getSelection() as Selection;
			const rstr = selection.toString();
			saveCaretPosition(box)?.();
			gatherBoxContents(false);
			const content = this.rawString;
			if (content) {
				let txti = text;
				if (trim && TypeBox.inPre()) {
					txti = txti.replace(/\n$/, "");
				}
				const [first, ...ends] = content.split(txti);
				const end = ends.join(txti);
				if (rstr) {
					const tw = text.split(rstr);
					tw.pop();
					text = tw.join("");
				}
				const boxText = first + txti + txt + (end ?? "");
				box.innerHTML = "";
				this.txt = boxText.split("");
				box.append(this.makeHTML({keep: true}));
				const len = txti.length + txt.length;
				text = boxText;

				this.boxupdate(len, false, 0);
			} else {
				this.txt = txt.split("");
				box.innerHTML = "";
				box.append(this.makeHTML({keep: true}));
				text = txt;
				this.boxupdate(txt.length, false, 0);
			}
		};

		box.onkeyup = (_) => {
			if (_.isComposing) return;
			gatherBoxContents(_.key === "Backspace");
		};
		box.onkeydown = (_) => {
			if (isFirefox && _.key === "Enter" && (TypeBox.inPre() || _.shiftKey)) {
				_.preventDefault();
				_.stopImmediatePropagation();

				insertText("\n", true);

				return;
			}
		};
		box.addEventListener("compositionend", (_) => {
			gatherBoxContents(false);
		});
		box.onpaste = (_) => {
			if (!_.clipboardData) return;
			const types = _.clipboardData.types;
			if (types.includes("Files")) {
				_.preventDefault();
				return;
			}

			if (types.includes("text/html")) {
				const data = _.clipboardData.getData("text/html");
				const html = new DOMParser().parseFromString(data, "text/html");
				const txt = MarkDown.gatherBoxText(html.body);
				insertText(txt);
				_.preventDefault();
			} else if (types.includes("text/plain")) {
				//Allow the paste like normal
			} else {
				_.preventDefault();
			}
		};
	}
	customBox?: [(arg1: string) => HTMLElement, (arg1: HTMLElement) => string];
	clearCustom() {
		this.customBox = undefined;
	}
	setCustomBox(
		stringToHTML: (arg1: string) => HTMLElement,
		HTMLToString = MarkDown.gatherBoxText.bind(MarkDown),
	) {
		this.customBox = [stringToHTML, HTMLToString];
	}
	boxEnabled = true;
	boxupdate(
		offset = 0,
		allowLazy = true,
		computedLength: void | number = undefined,
		backspace = false,
	) {
		if (!this.boxEnabled) return;
		const box = this.box.deref();
		if (!box) return;
		let restore: undefined | ((backspace: boolean) => void);
		if (this.customBox) {
			restore = saveCaretPosition(box, offset, this.customBox[1], computedLength);
		} else {
			restore = saveCaretPosition(
				box,
				offset,
				MarkDown.gatherBoxText.bind(MarkDown),
				computedLength,
			);
		}

		if (this.customBox) {
			//TODO maybe the custom logic applies here as well, but that's a later thing
			box.innerHTML = "";
			box.append(this.customBox[0](this.rawString));
		} else {
			//console.time();
			const html = this.makeHTML({keep: true});
			const condition =
				html.childNodes.length == 1 &&
				html.childNodes[0].childNodes.length === 1 &&
				html.childNodes[0].childNodes[0];
			//console.log(box.cloneNode(true), html.cloneNode(true));
			if (isFirefox) allowLazy = false;
			//TODO this may be slow, may want to check in on this in the future if it is
			if ((!box.hasChildNodes() || html.isEqualNode(Array.from(box.childNodes)[0])) && allowLazy) {
				//console.log("no replace needed");
			} else {
				if (
					!(box.childNodes.length === 1 && box.childNodes[0] instanceof Text && condition) ||
					!allowLazy
				) {
					box.innerHTML = "";
					box.append(html);
				} else {
					//console.log("empty replace not happened");
				}
			}
			//console.timeEnd();
		}
		if (restore) {
			restore(backspace);
		}
		this.onUpdate(text, formatted);
	}
	static gatherBoxText(element: HTMLElement): string {
		if (element.tagName.toLowerCase() === "img") {
			return (element as HTMLImageElement).alt;
		}
		if (element.tagName.toLowerCase() === "br") {
			return "\n";
		}
		if (element.hasAttribute("real")) {
			return element.getAttribute("real") as string;
		}
		if (element.tagName.toLowerCase() === "pre" || element.tagName.toLowerCase() === "samp") {
			formatted = true;
		} else {
			formatted = false;
		}
		let build = "";
		const arr = Array.from(element.childNodes);
		for (const thing of arr) {
			if (thing instanceof Text) {
				const text = thing.textContent;
				build += text;
				if (element.tagName.toLowerCase() === "pre") {
					if (build.endsWith("\n") && isFirefox) {
						build = build.replace(/\n$/, "");
					}
				}

				continue;
			}
			const text = this.gatherBoxText(thing as HTMLElement);
			if (text) {
				build += text;
			}
		}
		return build;
	}
	static trustedDomains = this.getTrusted();
	static getTrusted() {
		const domains = localStorage.getItem("trustedDomains");
		if (domains) {
			return new Set(JSON.parse(domains) as string[]);
		}
		return new Set([location.host, "fermi.chat", "blog.fermi.chat"]);
	}
	static saveTrusted(remove = false) {
		if (!remove) {
			this.trustedDomains = this.trustedDomains.union(this.getTrusted());
		}
		const domains = JSON.stringify([...this.trustedDomains]);

		localStorage.setItem("trustedDomains", domains);
	}
	static safeLink(
		elm: HTMLElement,
		url: string,
		localuser: Localuser | null = null,
	): string | void {
		if (elm instanceof HTMLAnchorElement) {
			elm.rel = "noopener noreferrer";
		}
		if (URL.canParse(url)) {
			const Url = new URL(url);
			const trusted = ["http:", "https:"];
			if (!trusted.includes(Url.protocol)) return;
			if (localuser) {
				const [_, _2, ...path] = Url.pathname.split("/");

				const guild = localuser.guilds.get(path[0]);
				const channel = guild?.getChannel(path[1]);
				if (channel) {
					const message = isNaN(+path[2]) ? undefined : path[2];
					elm.onmouseup = (_) => {
						if (message) channel.focus(message);
						else channel.goToBottom();
					};
					if (message) {
						return I18n.messageLink(channel.name);
					} else {
						return I18n.channelLink(channel.name);
					}
				}
			}
			if (elm instanceof HTMLAnchorElement && this.trustedDomains.has(Url.host)) {
				elm.href = url;
				elm.target = "_blank";
				return;
			}
			elm.onmouseup = (_) => {
				let parent: HTMLElement | null = elm;
				while (parent) {
					if (parent.classList.contains("unspoiled")) break;
					if (parent.classList.contains("spoiler")) return;
					parent = parent.parentElement;
				}
				if (_.button === 2) return;
				function open() {
					const proxy = window.open(url, "_blank");
					if (proxy && _.button === 1) {
						proxy.focus();
					} else if (proxy) {
						window.focus();
					}
				}
				if (this.trustedDomains.has(Url.host)) {
					open();
				} else {
					const full = new Dialog("");
					full.options.addTitle(I18n.leaving());
					full.options.addText(I18n.goingToURL(Url.host));
					const options = full.options.addOptions("", {ltr: true});
					options.addButtonInput("", I18n.nevermind(), () => full.hide());
					options.addButtonInput("", I18n.goThere(), () => {
						open();
						full.hide();
					});
					options.addButtonInput("", I18n.goThereTrust(), () => {
						open();
						full.hide();
						this.trustedDomains.add(Url.host);
						this.saveTrusted();
					});
					full.show();
					full.background.deref()!.style.zIndex = "300";
				}
			};
		} else {
			throw new Error(url + " is not a valid URL");
		}
	}
	/*
	static replace(base: HTMLElement, newelm: HTMLElement) {
	const basechildren = base.children;
	const newchildren = newelm.children;
	for (const thing of Array.from(newchildren)) {
	base.append(thing);
	}
	}
	*/
}

//solution from https://stackoverflow.com/questions/4576694/saving-and-restoring-caret-position-for-contenteditable-div
let text = "";
let formatted = false;
function saveCaretPosition(
	context: HTMLElement,
	offset = 0,
	txtLengthFunc = MarkDown.gatherBoxText.bind(MarkDown),
	computedLength: void | number = undefined,
) {
	const selection = window.getSelection() as Selection;
	if (!selection) return;
	try {
		const range = selection.getRangeAt(0);

		let base = selection.anchorNode as Node;
		range.setStart(base, 0);
		let baseString: string;
		let i = 0;
		const index = selection.focusOffset;

		for (const thing of Array.from(base.childNodes)) {
			if (i === index) {
				base = thing;
				break;
			}
			i++;
		}
		const prev = base.previousSibling;
		let len = 0;
		if ((!prev || prev instanceof HTMLBRElement) && base instanceof HTMLBRElement) {
			len--;
		}
		if (
			!(base instanceof Text) &&
			!(
				base instanceof HTMLSpanElement &&
				base.className === "" &&
				base.children.length == 0 &&
				!(base instanceof HTMLBRElement)
			)
		) {
			if (base instanceof HTMLElement) {
				baseString = txtLengthFunc(base);
			} else {
				baseString = base.textContent || "";
			}
		} else {
			baseString = selection.toString();
		}
		range.setStart(context, 0);

		let build = "";
		//I think this is working now :3
		function crawlForText(context: Node) {
			//@ts-ignore
			const children = [...context.childNodes];
			if (children.length === 1 && children[0] instanceof Text) {
				if (selection.containsNode(context, false)) {
					build += txtLengthFunc(context as HTMLElement);
				} else if (selection.containsNode(context, true)) {
					if (context.contains(base) || context === base || base.contains(context)) {
						build += baseString;
					} else {
						build += context.textContent;
					}
				} else {
					console.error(context);
				}
				return;
			}
			for (const node of children as Node[]) {
				if (selection.containsNode(node, false)) {
					if (node instanceof HTMLElement) {
						build += txtLengthFunc(node);
					} else {
						build += node.textContent;
					}
				} else if (selection.containsNode(node, true)) {
					if (node instanceof HTMLElement) {
						crawlForText(node);
					} else {
						build += node.textContent;
					}
				} else {
					//console.error(node, "This shouldn't happen");
				}
			}
		}
		crawlForText(context);
		if (baseString === "\n") {
			//build += baseString;
		}
		text = build;
		len += build.length;
		if (computedLength !== undefined) {
			len = computedLength;
		}
		len = Math.min(len, txtLengthFunc(context).length);
		len += offset;

		return function restore(_backspace = false) {
			if (!selection) return;
			const pos = getTextNodeAtPosition(context, len, txtLengthFunc);
			if (
				pos.node instanceof Text &&
				pos.node.textContent === "\n" &&
				pos.node.nextSibling &&
				isFirefox
			) {
				if (pos.node.nextSibling instanceof Text && pos.node.nextSibling.textContent === "\n") {
					pos.position = 1;
				} else {
					pos.node = pos.node.nextSibling;
					pos.position = 0;
				}
			}
			selection.removeAllRanges();
			const range = new Range();
			range.setStart(pos.node, pos.position);
			selection.addRange(range);
		};
	} catch {
		return undefined;
	}
}

function getTextNodeAtPosition(
	root: Node,
	index: number,
	txtLengthFunc = MarkDown.gatherBoxText.bind(MarkDown),
): {
	node: Node;
	position: number;
} {
	if (index === 0) {
		return {
			node: root,
			position: 0,
		};
	}
	if (root instanceof Text) {
		return {
			node: root,
			position: index,
		};
	} else if (root instanceof HTMLBRElement) {
		return {
			node: root,
			position: 0,
		};
	} else if (root instanceof HTMLElement && root.hasAttribute("real")) {
		return {
			node: root,
			position: -1,
		};
	}

	let lastElm: Node = root;
	for (const node of root.childNodes as unknown as Node[]) {
		lastElm = node;
		let len: number;
		if (node instanceof HTMLElement) {
			len = txtLengthFunc(node).length;
		} else {
			len = (node.textContent || "").length;
		}
		if (len <= index && (len < index || len !== 0)) {
			index -= len;
			if (index === 0) {
				let nodey = node;
				let bad = false;
				while (nodey.childNodes.length) {
					nodey = Array.from(nodey.childNodes).at(-1) as ChildNode;
					if (nodey instanceof HTMLElement && nodey.contentEditable === "false") {
						bad = true;
						break;
					}
				}
				if (
					!(nodey instanceof HTMLBRElement) &&
					(!(nodey instanceof HTMLElement) || nodey.contentEditable === "false") &&
					!bad
				) {
					return {
						node: nodey,
						position: (nodey.textContent || "").length,
					};
				}
			}
		} else {
			const returny = getTextNodeAtPosition(node, index);
			if (returny.position === -1) {
				index = 0;
				continue;
			}
			return returny;
		}
	}
	if (!(lastElm instanceof HTMLElement && lastElm.hasAttribute("real"))) {
		while (lastElm && !(lastElm instanceof Text || lastElm instanceof HTMLBRElement)) {
			lastElm = lastElm.childNodes[lastElm.childNodes.length - 1];
		}
		if (lastElm) {
			const position = (lastElm.textContent as string).length;
			return {
				node: lastElm,
				position,
			};
		}
	}
	const span = document.createElement("span");
	root.appendChild(span);
	return {
		node: span,
		position: 0,
	};
}
export {MarkDown, saveCaretPosition, getTextNodeAtPosition};
