<template v-slot="{ mfmTree }">
    <span>
        <component :is="mfmTree" />
    </span>
</template>

<script lang="ts" setup>
import { h, shallowRef, VNodeArrayChildren, VNodeChild, watch } from "vue";
import * as mfm from "mfm-js";
import MkUrl from "@/components/global/MkUrl.vue";
import MkLink from "@/components/MkLink.vue";
import MagMention from "@/components/MagMention.vue";
import MagMatrixMention from "@/components/MagMatrixMention.vue";
import { concat } from "@/scripts/array";
import MkFormula from "@/components/MkFormula.vue";
import MkCode from "@/components/MkCode.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 MagEmoji from "@/components/global/MagEmoji.vue";
import {
    magConvertReaction,
    magIsMissingEmoji,
    magReactionEquals,
    magTransProperty,
} from "@/scripts-mag/mag-util";
import * as Misskey from "calckey-js";
import { packed } from "magnetar-common";
import {
    magnetarMarkdownToMfm,
    MagnetarParseError,
    MagNode,
    parseMagnetarMarkdownXml,
} from "@/scripts-mag/mmm-util";
import { Result } from "@/types/result";

const props = withDefaults(
    defineProps<{
        mm?: string;
        text: string;
        plain?: boolean;
        nowrap?: boolean;
        author?: any;
        customEmojis?: (packed.PackEmojiBase | Misskey.entities.CustomEmoji)[];
        isNote?: boolean;
    }>(),
    {
        plain: false,
        nowrap: false,
        author: null,
        isNote: true,
    },
);

function render() {
    let ast: MagNode[];

    let result: Result<MagNode[], MagnetarParseError> | null = null;
    if (props.mm) {
        result = parseMagnetarMarkdownXml(props.mm).flatMap(
            magnetarMarkdownToMfm,
        );
    }

    if (result && result.isOk()) {
        ast = result.unwrap();
    } else if (!props.text) {
        return h("span");
    } else {
        const isPlain = props.plain;
        ast = (isPlain ? mfm.parseSimple : mfm.parse)(props.text);
    }

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

    const validNumber = (n: string | boolean | null | undefined) => {
        if (n == null) return null;
        if (typeof n === "boolean") 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 genEl = (ast: MagNode[]) =>
        concat(
            ast.map((token, index): VNodeChild[] => {
                switch (token.type) {
                    case "text": {
                        const text = token.props.text.replace(
                            /(\r\n|\n|\r)/g,
                            "\n",
                        );

                        if (!props.plain) {
                            const res = [] as VNodeArrayChildren;
                            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": {
                        let style: string | null = null;
                        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 "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": {
                                const x = Math.min(
                                    parseFloat("" + token.props.args.x ?? "1"),
                                    5,
                                );
                                const y = Math.min(
                                    parseFloat("" + token.props.args.y ?? "1"),
                                    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 "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) {
                            return [
                                h(
                                    "span",
                                    {
                                        style: `display: inline-block;${style}`,
                                    },
                                    genEl(token.children),
                                ),
                            ];
                        }

                        return [
                            h("span", {}, [
                                "$[",
                                token.props.name,
                                " ",
                                ...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(MagMention, {
                                key: Math.random(),
                                username: token.props.username,
                                host:
                                    (token.props.host == null &&
                                    props.author &&
                                    props.author.host != null
                                        ? props.author.host
                                        : token.props.host) || host,
                            }),
                        ];
                    }

                    case "matrixMention": {
                        return [
                            h(MagMatrixMention, {
                                key: Math.random(),
                                username: token.props.username,
                                host: token.props.host,
                            }),
                        ];
                    }

                    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 (!props.nowrap) {
                            return [h("blockquote", genEl(token.children))];
                        } else {
                            return [
                                h(
                                    "span",
                                    {
                                        class: "quote",
                                    },
                                    genEl(token.children),
                                ),
                            ];
                        }
                    }

                    case "emojiCode": {
                        const shortcode = `:${token.props.name}:`;
                        const emoji = magConvertReaction(
                            shortcode,
                            (name, host) =>
                                props.customEmojis?.find((e) =>
                                    magReactionEquals(
                                        magConvertReaction(
                                            `:${magTransProperty(
                                                e,
                                                "shortcode",
                                                "name",
                                            )}:`,
                                        ),
                                        { name, host, url: null! },
                                    ),
                                )?.url ?? null!,
                        );

                        if (magIsMissingEmoji(emoji)) {
                            return [shortcode];
                        }

                        return [
                            h(MagEmoji, {
                                key: Math.random(),
                                emoji,
                                normal: props.plain,
                            }),
                        ];
                    }

                    case "unicodeEmoji": {
                        return [
                            h(MagEmoji, {
                                key: Math.random(),
                                emoji: token.props.emoji,
                                normal: props.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": {
                        const sentinel = "#";
                        let ast2 = (props.plain ? mfm.parseSimple : mfm.parse)(
                            token.props.content + sentinel,
                        );

                        const lastNode = ast2[ast2.length - 1];
                        if (
                            lastNode.type === "text" &&
                            (
                                lastNode.props as
                                    | mfm.MfmText["props"]
                                    | undefined
                            )?.text?.endsWith(sentinel)
                        ) {
                            lastNode.props.text = lastNode.props.text.slice(
                                0,
                                -1,
                            );
                        }

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

                        return [prefix, ...genEl(ast2)];
                    }

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

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

                        return [];
                    }
                }
            }),
        );

    return h("span", genEl(ast));
}

const mfmTree = shallowRef<VNodeChild>(props.text);
watch(
    () => props.text,
    () => {
        mfmTree.value = render();
    },
    {
        immediate: true,
    },
);
</script>
