import { defineComponent, h } from "vue";
import * as mfm from "mfm-js";
import type { VNode } from "vue";
import CkFollowMouse from "./CkFollowMouse.vue";
import MkUrl from "@/components/global/MkUrl.vue";
import MkTime from '@/components/global/MkTime.vue';
import MkLink from "@/components/MkLink.vue";
import MkMention from "@/components/MkMention.vue";
import MkEmoji from "@/components/global/MkEmoji.vue";
import { concat } from "@/scripts/array";
import MkFormula from "@/components/MkFormula.vue";
import MkCode from "@/components/MkCode.vue";
import MkGoogle from "@/components/MkGoogle.vue";
import MkSparkle from "@/components/MkSparkle.vue";
import MkA from "@/components/global/MkA.vue";
import { host } from "@/config";
import { reducedMotion } from "@/scripts/reduced-motion";
import { defaultStore } from "@/store";
import { safeParseFloat } from "@/scripts/safe-parse";

export default defineComponent({
	props: {
		text: {
			type: String,
			required: true,
		},
		plain: {
			type: Boolean,
			default: false,
		},
		nowrap: {
			type: Boolean,
			default: false,
		},
		author: {
			type: Object,
			default: null,
		},
		i: {
			type: Object,
			default: null,
		},
		customEmojis: {
			required: false,
		},
		isNote: {
			type: Boolean,
			default: true,
		},
	},

	render() {
		if (this.text == null || this.text === "") return;

		const isPlain = this.plain;

		const ast = (isPlain ? mfm.parseSimple : mfm.parse)(this.text);

		const validTime = (t: string | null | undefined) => {
			if (t == null) return null;
			return t.match(/^[0-9.]+s$/) ? t : null;
		};

		const validNumber = (n: string | null | undefined) => {
			if (n == null) return null;
			const parsed = parseFloat(n);
			return !isNaN(parsed) && isFinite(parsed) && parsed > 0;
		};
		// const validEase = (e: string | null | undefined) => {
		// 	if (e == null) return null;
		// 	return e.match(/(steps)?\(-?[0-9.]+,-?[0-9.]+,-?[0-9.]+,-?[0-9.]+\)/)
		// 		? (e.startsWith("steps") ? e : "cubic-bezier" + e)
		// 		: null
		// }

		const validColor = (c: unknown): string | null => {
			if (typeof c !== 'string') return null;
			return c.match(/^[0-9a-f]{3,6}$/i) ? c : null;
		};

		const genEl = (ast: mfm.MfmNode[]) =>
			concat(
				ast.map((token, index): VNode[] => {
					switch (token.type) {
						case "text": {
							const text = token.props.text.replace(/(\r\n|\n|\r)/g, "\n");

							if (!this.plain) {
								const res = [];
								for (const t of text.split("\n")) {
									res.push(h("br"));
									res.push(t);
								}
								res.shift();
								return res;
							} else {
								return [text.replace(/\n/g, " ")];
							}
						}

						case "bold": {
							return [h("b", genEl(token.children))];
						}

						case "strike": {
							return [h("del", genEl(token.children))];
						}

						case "italic": {
							return h(
								"i",
								{
									style: "font-style: oblique;",
								},
								genEl(token.children),
							);
						}

						case "fn": {
							// TODO: CSSを文字列で組み立てていくと token.props.args.~~~ 経由でCSSインジェクションできるのでよしなにやる
							let style: string;
							switch (token.props.name) {
								case "tada": {
									const speed = validTime(token.props.args.speed) || "1s";
									const delay = validTime(token.props.args.delay) || "0s";
									const loop = validNumber(token.props.args.loop) || "infinite";
									// const ease = validEase(token.props.args.ease) || "linear";
									style = `font-size: 150%; animation: tada ${speed} ${delay} linear ${loop} both;`;
									break;
								}
								case "jelly": {
									const speed = validTime(token.props.args.speed) || "1s";
									const delay = validTime(token.props.args.delay) || "0s";
									const loop = validNumber(token.props.args.loop) || "infinite";
									style = `animation: mfm-rubberBand ${speed} ${delay} linear ${loop} both;`;
									break;
								}
								case "twitch": {
									const speed = validTime(token.props.args.speed) || "0.5s";
									const delay = validTime(token.props.args.delay) || "0s";
									const loop = validNumber(token.props.args.loop) || "infinite";
									style = `animation: mfm-twitch ${speed} ${delay} ease ${loop};`;
									break;
								}
								case "shake": {
									const speed = validTime(token.props.args.speed) || "0.5s";
									const delay = validTime(token.props.args.delay) || "0s";
									const loop = validNumber(token.props.args.loop) || "infinite";
									style = `animation: mfm-shake ${speed} ${delay} ease ${loop};`;
									break;
								}
								case "spin": {
									const direction = token.props.args.left
										? "reverse"
										: token.props.args.alternate
										? "alternate"
										: "normal";
									const anime = token.props.args.x
										? "mfm-spinX"
										: token.props.args.y
										? "mfm-spinY"
										: "mfm-spin";
									const speed = validTime(token.props.args.speed) || "1.5s";
									const delay = validTime(token.props.args.delay) || "0s";
									const loop = validNumber(token.props.args.loop) || "infinite";
									style = `animation: ${anime} ${speed} ${delay} linear ${loop}; animation-direction: ${direction};`;
									break;
								}
								case "jump": {
									const speed = validTime(token.props.args.speed) || "0.75s";
									const delay = validTime(token.props.args.delay) || "0s";
									const loop = validNumber(token.props.args.loop) || "infinite";
									style = `animation: mfm-jump ${speed} ${delay} linear ${loop};`;
									break;
								}
								case "bounce": {
									const speed = validTime(token.props.args.speed) || "0.75s";
									const delay = validTime(token.props.args.delay) || "0s";
									const loop = validNumber(token.props.args.loop) || "infinite";
									style = `animation: mfm-bounce ${speed} ${delay} linear ${loop}; transform-origin: center bottom;`;
									break;
								}
								case "rainbow": {
									const speed = validTime(token.props.args.speed) || "1s";
									const delay = validTime(token.props.args.delay) || "0s";
									const loop = validNumber(token.props.args.loop) || "infinite";
									style = `animation: mfm-rainbow ${speed} ${delay} linear ${loop};`;
									break;
								}
								case "sparkle": {
									if (reducedMotion()) {
										return genEl(token.children);
									}
									return h(MkSparkle, {}, genEl(token.children));
								}
								case "fade": {
									const direction = token.props.args.out
										? "alternate-reverse"
										: "alternate";
									const speed = validTime(token.props.args.speed) || "1.5s";
									const delay = validTime(token.props.args.delay) || "0s";
									const loop = validNumber(token.props.args.loop) || "infinite";
									style = `animation: mfm-fade ${speed} ${delay} linear ${loop}; animation-direction: ${direction};`;
									break;
								}
								case "flip": {
									const transform =
										token.props.args.h && token.props.args.v
											? "scale(-1, -1)"
											: token.props.args.v
											? "scaleY(-1)"
											: "scaleX(-1)";
									style = `transform: ${transform};`;
									break;
								}
								case "x2": {
									return h(
										"span",
										{
											class: "mfm-x2",
										},
										genEl(token.children),
									);
								}
								case "x3": {
									return h(
										"span",
										{
											class: "mfm-x3",
										},
										genEl(token.children),
									);
								}
								case "x4": {
									return h(
										"span",
										{
											class: "mfm-x4",
										},
										genEl(token.children),
									);
								}
								case "font": {
									const family = token.props.args.serif
										? "serif"
										: token.props.args.monospace
										? "monospace"
										: token.props.args.cursive
										? "cursive"
										: token.props.args.fantasy
										? "fantasy"
										: token.props.args.emoji
										? "emoji"
										: token.props.args.math
										? "math"
										: null;
									if (family) style = `font-family: ${family};`;
									break;
								}
								case "blur": {
									return h(
										"span",
										{
											class: "_blur_text",
										},
										genEl(token.children),
									);
								}
								case "rotate": {
									const rotate = token.props.args.x
										? "perspective(128px) rotateX"
										: token.props.args.y
										? "perspective(128px) rotateY"
										: "rotate";
									const degrees = parseInt(token.props.args.deg) || "90";
									style = `transform: ${rotate}(${degrees}deg); transform-origin: center center;`;
									break;
								}
								case 'followmouse': {
									// Make sure advanced MFM is on and that reduced motion is off
			
									let x = (!!token.props.args.x);
									let y = (!!token.props.args.y);
			
									if (!x && !y) {
										x = true;
										y = true;
									}
			
									return h(CkFollowMouse, {
										x: x,
										y: y,
										speed: validTime(token.props.args.speed) ?? '0.1s',
										rotateByVelocity: !!token.props.args.rotateByVelocity,
									}, genEl(token.children));
								}
								case "position": {
									const x = parseFloat(token.props.args.x ?? "0");
									const y = parseFloat(token.props.args.y ?? "0");
									style = `transform: translateX(${x}em) translateY(${y}em);`;
									break;
								}
								case "crop": {
									const top = parseFloat(token.props.args.top ?? "0");
									const right = parseFloat(token.props.args.right ?? "0");
									const bottom = parseFloat(token.props.args.bottom ?? "0");
									const left = parseFloat(token.props.args.left ?? "0");
									style = `clip-path: inset(${top}% ${right}% ${bottom}% ${left}%);`;
									break;
								}
								case "scale": {
									let x = Math.min(parseFloat(token.props.args.x ?? "1"), 5);
									let y = Math.min(parseFloat(token.props.args.y ?? "1"), 5);
									if (x < -5) x = -5;
									if (y < -5) y = -5;
									style = `transform: scale(${x}, ${y});`;
									break;
								}
								case "fg": {
									let color = token.props.args.color;
									if (!/^[0-9a-f]{3,6}$/i.test(color)) color = "f00";
									style = `color: #${color};`;
									break;
								}
								case "bg": {
									let color = token.props.args.color;
									if (!/^[0-9a-f]{3,6}$/i.test(color)) color = "f00";
									style = `background-color: #${color};`;
									break;
								}
								case 'border': {
									let color = validColor(token.props.args.color);
									color = color ? `#${color}` : 'var(--accent)';
									let b_style = token.props.args.style;
									if (
										typeof b_style !== 'string' ||
										!['hidden', 'dotted', 'dashed', 'solid', 'double', 'groove', 'ridge', 'inset', 'outset']
											.includes(b_style)
									) b_style = 'solid';
									const width = safeParseFloat(token.props.args.width) ?? 1;
									const radius = safeParseFloat(token.props.args.radius) ?? 0;
									style = `border: ${width}px ${b_style} ${color}; border-radius: ${radius}px;${token.props.args.noclip ? '' : ' overflow: clip;'}`;
									break;
								}
								case 'ruby': {
									if (token.children.length === 1) {
										const child = token.children[0];
										let text = child.type === 'text' ? child.props.text : '';
										return h('ruby', {}, [text.split(' ')[0], h('rt', text.split(' ')[1])]);
									} else {
										const rt = token.children.at(-1)!;
										let text = rt.type === 'text' ? rt.props.text : '';
										return h('ruby', {}, [...genEl(token.children.slice(0, token.children.length - 1), scale), h('rt', text.trim())]);
									}
								}
								case 'unixtime': {
									const child = token.children[0];
									const unixtime = parseInt(child.type === 'text' ? child.props.text : '');
									return h('span', {
										style: 'display: inline-block; font-size: 90%; border: solid 1px var(--divider); border-radius: var(--radius-ellipse); padding: 4px 10px 4px 6px;',
									}, [
										h('i', {
											class: 'ph-clock ph-bold ph-lg',
											style: 'margin-right: 0.25em;',
										}),
										h(MkTime, {
											key: Math.random(),
											time: unixtime * 1000,
											mode: 'detail',
										}),
									]);
								}
								case "small": {
									return h(
										"small",
										{
											style: "opacity: 0.7;",
										},
										genEl(token.children),
									);
								}
								case "center": {
									return h(
										"div",
										{
											style: "text-align: center;",
										},
										genEl(token.children),
									);
								}
							}
							if (style == null) {
								return h("span", {}, [
									"$[",
									token.props.name,
									" ",
									...genEl(token.children),
									"]",
								]);
							} else {
								return h(
									"span",
									{
										style: `display: inline-block;${style}`,
									},
									genEl(token.children),
								);
							}
						}

						case "small": {
							return [
								h(
									"small",
									{
										style: "opacity: 0.7;",
									},
									genEl(token.children),
								),
							];
						}

						case "center": {
							return [
								h(
									"div",
									{
										style: "text-align: center;",
									},
									genEl(token.children),
								),
							];
						}

						case "url": {
							return [
								h(MkUrl, {
									key: Math.random(),
									url: token.props.url,
									rel: "nofollow noopener",
								}),
							];
						}

						case "link": {
							return [
								h(
									MkLink,
									{
										key: Math.random(),
										url: token.props.url,
										rel: "nofollow noopener",
									},
									genEl(token.children),
								),
							];
						}

						case "mention": {
							return [
								h(MkMention, {
									key: Math.random(),
									host:
										(token.props.host == null &&
										this.author &&
										this.author.host != null
											? this.author.host
											: token.props.host) || host,
									username: token.props.username,
								}),
							];
						}

						case "hashtag": {
							return [
								h(
									MkA,
									{
										key: Math.random(),
										to: `/tags/${encodeURIComponent(token.props.hashtag)}`,
										style: "color:var(--hashtag);",
									},
									`#${token.props.hashtag}`,
								),
							];
						}

						case "blockCode": {
							return [
								h(MkCode, {
									key: Math.random(),
									code: token.props.code,
									lang: token.props.lang,
								}),
							];
						}

						case "inlineCode": {
							return [
								h(MkCode, {
									key: Math.random(),
									code: token.props.code,
									inline: true,
								}),
							];
						}

						case "quote": {
							if (!this.nowrap) {
								return [h("blockquote", genEl(token.children))];
							} else {
								return [
									h(
										"span",
										{
											class: "quote",
										},
										genEl(token.children),
									),
								];
							}
						}

						case "emojiCode": {
							return [
								h(MkEmoji, {
									key: Math.random(),
									emoji: `:${token.props.name}:`,
									customEmojis: this.customEmojis,
									normal: this.plain,
								}),
							];
						}

						case "unicodeEmoji": {
							return [
								h(MkEmoji, {
									key: Math.random(),
									emoji: token.props.emoji,
									customEmojis: this.customEmojis,
									normal: this.plain,
								}),
							];
						}

						case "mathInline": {
							return [
								h(MkFormula, {
									key: Math.random(),
									formula: token.props.formula,
									block: false,
								}),
							];
						}

						case "mathBlock": {
							return [
								h(MkFormula, {
									key: Math.random(),
									formula: token.props.formula,
									block: true,
								}),
							];
						}

						case "search": {
							// Disable "search" keyword
							// (see the issue #9816 on Codeberg)
							if (token.props.content.slice(-6).toLowerCase() === "search") {
								const sentinel = "#";
								let ast2 = (isPlain ? mfm.parseSimple : mfm.parse)(
									token.props.content.slice(0, -6) + sentinel,
								);
								if (
									ast2[ast2.length - 1].type === "text" &&
									ast2[ast2.length - 1].props.text.endsWith(sentinel)
								) {
									ast2[ast2.length - 1].props.text = ast2[
										ast2.length - 1
									].props.text.slice(0, -1);
								} else {
									// I don't think this scope is reachable
									console.warn(
										"Something went wrong while parsing MFM. Please send a bug report, if possible.",
									);
								}

								let prefix = "\n";
								if (
									index === 0 ||
									[
										"blockCode",
										"center",
										"mathBlock",
										"quote",
										"search",
									].includes(ast[index - 1].type)
								) {
									prefix = "";
								}

								return [
									prefix,
									...genEl(ast2),
									`${token.props.content.slice(-6)}\n`,
								];
							}

							return [
								h(MkGoogle, {
									key: Math.random(),
									q: token.props.query,
								}),
							];
						}

						case "plain": {
							return [h("span", genEl(token.children))];
						}

						default: {
							console.error("unrecognized ast type:", token.type);

							return [];
						}
					}
				}),
			);

		// Parse ast to DOM
		return h("span", genEl(ast));
	},
});
