using System.Text.RegularExpressions;
using AngleSharp;
using AngleSharp.Dom;
using AngleSharp.Text;
using Iceshrimp.MfmSharp;
using Iceshrimp.Shared.Schemas.Web;
using Microsoft.AspNetCore.Components;

namespace Iceshrimp.Frontend.Core.Miscellaneous;

public static partial class MfmRenderer
{
	public static async Task<MarkupString> RenderStringAsync(
		string text, List<EmojiResponse> emoji, string accountDomain, bool simple = false
	)
	{
		var res         = MfmParser.Parse(text, simple);
		var context     = BrowsingContext.New();
		var document    = await context.OpenNewAsync();
		var renderedMfm = RenderMultipleNodes(res, document, emoji, accountDomain, simple);
		var html        = renderedMfm.ToHtml();
		return new MarkupString(html);
	}

	private static INode RenderMultipleNodes(
		IEnumerable<IMfmNode> nodes, IDocument document, List<EmojiResponse> emoji, string accountDomain, bool simple
	)
	{
		var el = document.CreateElement("span");
		el.SetAttribute("mfm", "mfm");
		el.ClassName = "mfm";
		foreach (var node in nodes)
		{
			try
			{
				el.AppendNodes(RenderNode(node, document, emoji, accountDomain, simple));
			}
			catch (NotImplementedException e)
			{
				var fallback = document.CreateElement("span");
				fallback.TextContent = $"[Node type <{e.Message}> not implemented]";
				el.AppendNodes(fallback);
			}
		}

		return el;
	}

	private static INode RenderNode(
		IMfmNode node, IDocument document, List<EmojiResponse> emoji, string accountDomain, bool simple
	)
	{
		// Hard wrap makes this impossible to read
		// @formatter:off
		var rendered = node switch
		{
			MfmCenterNode _                     => MfmCenterNode(document),
			MfmCodeBlockNode mfmCodeBlockNode   => MfmCodeBlockNode(mfmCodeBlockNode, document),
			MfmMathBlockNode mfmMathBlockNode   => throw new NotImplementedException($"{mfmMathBlockNode.GetType()}"),
			MfmQuoteNode mfmQuoteNode           => MfmQuoteNode(mfmQuoteNode, document),
			IMfmBlockNode mfmBlockNode           => throw new NotImplementedException($"{mfmBlockNode.GetType()}"),
			MfmBoldNode mfmBoldNode             => MfmBoldNode(mfmBoldNode, document),
			MfmEmojiCodeNode mfmEmojiCodeNode   => MfmEmojiCodeNode(mfmEmojiCodeNode, document, emoji, simple),
			MfmFnNode mfmFnNode                 => MfmFnNode(mfmFnNode, document),
			MfmHashtagNode mfmHashtagNode       => MfmHashtagNode(mfmHashtagNode, document),
			MfmInlineCodeNode mfmInlineCodeNode => MfmInlineCodeNode(mfmInlineCodeNode, document),
			MfmItalicNode mfmItalicNode         => MfmItalicNode(mfmItalicNode, document),
			MfmLinkNode mfmLinkNode             => MfmLinkNode(mfmLinkNode, document),
			MfmInlineMathNode mfmInlineMathNode => throw new NotImplementedException($"{mfmInlineMathNode.GetType()}"),
			MfmMentionNode mfmMentionNode       => MfmMentionNode(mfmMentionNode, document, accountDomain),
			MfmPlainNode mfmPlainNode           => MfmPlainNode(mfmPlainNode, document),
			MfmSmallNode _                      => MfmSmallNode(document),
			MfmStrikeNode mfmStrikeNode         => MfmStrikeNode(mfmStrikeNode, document),
			MfmTextNode mfmTextNode             => MfmTextNode(mfmTextNode, document),
			MfmUrlNode mfmUrlNode               => MfmUrlNode(mfmUrlNode, document),
			IMfmInlineNode mfmInlineNode         => throw new NotImplementedException($"{mfmInlineNode.GetType()}"),
			_ => throw new ArgumentOutOfRangeException(nameof(node))
		};
		// @formatter:on

		if (node.Children.Length > 0 && rendered.ChildNodes.Length == 0)
		{
			foreach (var childNode in node.Children)
			{
				try
				{
					rendered.AppendNodes(RenderNode(childNode, document, emoji, accountDomain, simple));
				}
				catch (NotImplementedException e)
				{
					var fallback = document.CreateElement("span");
					fallback.TextContent = $"[Node type <{e.Message}> not implemented]";
					rendered.AppendNodes(fallback);
				}
			}
		}

		return rendered;
	}

	private static INode MfmPlainNode(MfmPlainNode _, IDocument document)
	{
		var el = document.CreateElement("span");
		el.ClassName = "plain";
		return el;
	}

	private static INode MfmCenterNode(IDocument document)
	{
		var el = document.CreateElement("div");
		el.SetAttribute("style", "text-align: center");
		return el;
	}

	private static INode MfmCodeBlockNode(MfmCodeBlockNode node, IDocument document)
	{
		var el = document.CreateElement("pre");
		el.ClassName = "code-pre";
		var childEl = document.CreateElement("code");
		childEl.TextContent = node.Code;
		el.AppendChild(childEl);
		return el;
	}

	private static INode MfmQuoteNode(MfmQuoteNode _, IDocument document)
	{
		var el = document.CreateElement("blockquote");
		el.ClassName = "quote-node";
		return el;
	}

	private static INode MfmInlineCodeNode(MfmInlineCodeNode node, IDocument document)
	{
		var el = document.CreateElement("code");
		el.TextContent = node.Code;
		return el;
	}

	private static INode MfmHashtagNode(MfmHashtagNode node, IDocument document)
	{
		var el = document.CreateElement("a");
		el.SetAttribute("href", $"/tags/{node.Hashtag}");
		el.ClassName   = "hashtag-node";
		el.TextContent = "#" + node.Hashtag;
		return el;
	}

	private static INode MfmLinkNode(MfmLinkNode node, IDocument document)
	{
		var el = document.CreateElement("a");
		el.SetAttribute("href", node.Url);
		el.SetAttribute("target", "_blank");
		el.ClassName   = "link-node";
		el.TextContent = node.Text;
		return el;
	}

	private static INode MfmItalicNode(MfmItalicNode _, IDocument document)
	{
		var el = document.CreateElement("span");
		el.SetAttribute("style", "font-style: italic");
		return el;
	}

	private static INode MfmEmojiCodeNode(
		MfmEmojiCodeNode node, IDocument document, List<EmojiResponse> emojiList, bool simple
	)
	{
		var el = document.CreateElement("span");
		el.ClassName = simple ? "emoji simple" : "emoji";

		var emoji = emojiList.Find(p => p.Name == node.Name);
		if (emoji is null)
		{
			el.TextContent = node.Name;
		}
		else
		{
			var image = document.CreateElement("img");
			image.SetAttribute("src", emoji.PublicUrl);
			image.SetAttribute("alt", node.Name);
			image.SetAttribute("title", $":{emoji.Name}:");
			el.AppendChild(image);
		}

		return el;
	}

	private static INode MfmUrlNode(MfmUrlNode node, IDocument document)
	{
		var el = document.CreateElement("a");
		el.SetAttribute("href", node.Url);
		el.SetAttribute("target", "_blank");
		el.ClassName   = "url-node";
		el.TextContent = node.Url;
		return el;
	}

	private static INode MfmBoldNode(MfmBoldNode _, IDocument document)
	{
		var el = document.CreateElement("strong");
		return el;
	}

	private static INode MfmSmallNode(IDocument document)
	{
		var el = document.CreateElement("small");
		el.SetAttribute("style", "opacity: 0.7;");
		return el;
	}

	private static INode MfmStrikeNode(MfmStrikeNode _, IDocument document)
	{
		var el = document.CreateElement("del");
		return el;
	}

	private static INode MfmTextNode(MfmTextNode node, IDocument document)
	{
		var el = document.CreateElement("span");
		el.TextContent = node.Text;
		return el;
	}

	private static INode MfmMentionNode(MfmMentionNode node, IDocument document, string accountDomain)
	{
		var link = document.CreateElement("a");
		link.SetAttribute("href",
		                  node.Host != null && node.Host != accountDomain
			                  ? $"/@{node.Acct}"
			                  : $"/@{node.User}");
		link.ClassName = "mention";
		var userPart = document.CreateElement("span");
		userPart.ClassName   = "user";
		userPart.TextContent = $"@{node.User}";
		link.AppendChild(userPart);
		if (node.Host != null && node.Host != accountDomain)
		{
			var hostPart = document.CreateElement("span");
			hostPart.ClassName   = "host";
			hostPart.TextContent = $"@{node.Host}";
			link.AppendChild(hostPart);
		}

		return link;
	}

	private static INode MfmFnNode(MfmFnNode node, IDocument document)
	{
		// Simplify node.Args structure to make it more readable in below functions
		var args = node.Args ?? [];
		
		return node.Name switch {
			"flip"     => MfmFnFlip(args, document),
			"font"     => MfmFnFont(args, document),
			"x2"       => MfmFnX(node.Name, document),
			"x3"       => MfmFnX(node.Name, document),
			"x4"       => MfmFnX(node.Name, document),
			"blur"     => MfmFnBlur(document),
			"jelly"    => MfmFnAnimation(node.Name, args, document),
			"tada"     => MfmFnAnimation(node.Name, args, document),
			"jump"     => MfmFnAnimation(node.Name, args, document, "0.75s"),
			"bounce"   => MfmFnAnimation(node.Name, args, document, "0.75s"),
			"spin"     => MfmFnSpin(args, document),
			"shake"    => MfmFnAnimation(node.Name, args, document, "0.5s"),
			"twitch"   => MfmFnAnimation(node.Name, args, document, "0.5s"),
			"rainbow"  => MfmFnAnimation(node.Name, args, document),
			"sparkle"  => throw new NotImplementedException($"{node.Name}"),
			"rotate"   => MfmFnRotate(args, document),
			"fade"     => MfmFnFade(args, document),
			"crop"     => MfmFnCrop(args, document),
			"position" => MfmFnPosition(args, document),
			"scale"    => MfmFnScale(args, document),
			"fg"       => MfmFnFg(args, document),
			"bg"       => MfmFnBg(args, document),
			"border"   => MfmFnBorder(args, document),
			"ruby"     => MfmFnRuby(node, document),
			"unixtime" => MfmFnUnixtime(node, document),
			"center"   => MfmCenterNode(document),
			"small"    => MfmSmallNode(document),
			_          => throw new NotImplementedException($"{node.Name}")
		};
	}

	private static INode MfmFnFlip(Dictionary<string, string?> args, IDocument document)
	{
		var el = document.CreateElement("span");

		if (args.ContainsKey("h") && args.ContainsKey("v"))
			el.ClassName = "fn-flip h v";
		else if (args.ContainsKey("v"))
			el.ClassName = "fn-flip v";
		else
			el.ClassName = "fn-flip h";

		return el;
	}
	
	private static INode MfmFnFont(Dictionary<string, string?> args, IDocument document)
	{
		var el = document.CreateElement("span");

		if (args.ContainsKey("serif"))
			el.SetAttribute("style", "font-family: serif;");
		else if (args.ContainsKey("monospace"))
			el.SetAttribute("style", "font-family: monospace;");
		else if (args.ContainsKey("cursive"))
			el.SetAttribute("style", "font-family: cursive;");
		else if (args.ContainsKey("fantasy"))
			el.SetAttribute("style", "font-family: fantasy;");

		return el;
	}

	private static INode MfmFnX(string name, IDocument document)
	{
		var el = document.CreateElement("span");

		var size = name switch
		{
			"x4" => "600%",
			"x3" => "400%",
			_    => "200%"
		};

		el.SetAttribute("style", $"display: inline-block; font-size: {size};");

		return el;
	}
	
	private static INode MfmFnBlur(IDocument document)
	{
		var el = document.CreateElement("span");

		el.ClassName = "fn-blur";

		return el;
	}

	private static INode MfmFnAnimation(
		string name, Dictionary<string, string?> args, IDocument document, string defaultSpeed = "1s"
	)
	{
		var el = document.CreateElement("span");

		el.ClassName = "fn-animation";

		var style = $"animation-name: fn-{name}-mfm;";
		style += args.TryGetValue("speed", out var speed)
			? $" animation-duration: {speed};"
			: $" animation-duration: {defaultSpeed};";
		style += args.TryGetValue("delay", out var delay) ? $" animation-delay: {delay};" : "";
		style += args.TryGetValue("loop", out var loop) ? $" animation-iteration-count: {loop};" : "";
		if (name == "tada") style += " font-size: 150%;";

		el.SetAttribute("style", style.Trim());

		return el;
	}
	
	private static INode MfmFnSpin(Dictionary<string, string?> args, IDocument document)
	{
		var el = document.CreateElement("span");

		el.ClassName = "fn-spin";

		var name = args.ContainsKey("y")
			? "fn-spin-y-mfm"
			: args.ContainsKey("x")
				? "fn-spin-x-mfm"
				: "fn-spin-mfm";
		var direction = args.ContainsKey("alternate")
			? "alternate"
			: args.ContainsKey("left")
				? "reverse"
				: "normal";

		var style = $"animation-name: {name}; animation-direction: {direction};";
		style += args.TryGetValue("speed", out var speed) ? $" animation-duration: {speed};" : "";
		style += args.TryGetValue("delay", out var delay) ? $" animation-delay: {delay};" : "";
		style += args.TryGetValue("loop", out var loop) ? $" animation-iteration-count: {loop};" : "";

		el.SetAttribute("style", style.Trim());

		return el;
	}

	private static INode MfmFnRotate(Dictionary<string, string?> args, IDocument document)
	{
		var el = document.CreateElement("span");

		var deg = args.GetValueOrDefault("deg") ?? "90";

		el.ClassName = "fn-rotate";
		if (args.ContainsKey("x"))
			el.SetAttribute("style", $"transform: perspective(120px) rotateX({deg}deg);");
		else
			el.SetAttribute("style", $"transform: rotate({deg}deg);");

		return el;
	}
	
	private static INode MfmFnFade(Dictionary<string, string?> args, IDocument document)
	{
		var el = document.CreateElement("span");

		el.ClassName = "fn-fade";

		var style = "";
		style += args.ContainsKey("out") ? "animation-direction: alternate-reverse; " : "";
		style += args.TryGetValue("speed", out var speed) ? $"animation-duration: {speed}; " : "";
		style += args.TryGetValue("delay", out var delay) ? $"animation-delay: {delay}; " : "";

		if (!string.IsNullOrWhiteSpace(style))
			el.SetAttribute("style", style.Trim());

		return el;
	}

	private static INode MfmFnCrop(Dictionary<string, string?> args, IDocument document)
	{
		var el = document.CreateElement("span");

		var inset = $"{args.GetValueOrDefault("top") ?? "0"}% {args.GetValueOrDefault("right") ?? "0"}% {args.GetValueOrDefault("bottom") ?? "0"}% {args.GetValueOrDefault("left") ?? "0"}%";
		el.SetAttribute("style", $"display: inline-block; clip-path: inset({inset});");

		return el;
	}
	
	private static INode MfmFnPosition(Dictionary<string, string?> args, IDocument document)
	{
		var el = document.CreateElement("span");

		var translateX = args.GetValueOrDefault("x") ?? "0";
		var translateY = args.GetValueOrDefault("y") ?? "0";
		el.SetAttribute("style", $"display: inline-block; transform: translateX({translateX}em) translateY({translateY}em);");

		return el;
	}
	
	private static INode MfmFnScale(Dictionary<string, string?> args, IDocument document)
	{
		var el = document.CreateElement("span");

		var scaleX = args.GetValueOrDefault("x") ?? "1";
		var scaleY = args.GetValueOrDefault("y") ?? "1";
		el.SetAttribute("style", $"display: inline-block; transform: scale({scaleX}, {scaleY});");

		return el;
	}

	[GeneratedRegex(@"^[0-9a-f]{3,6}$", RegexOptions.IgnoreCase)]
	private static partial Regex ColorRegex();
	
	private static bool ValidColor(string? color)
	{
		return color != null && ColorRegex().Match(color).Success;
	}
	
	private static INode MfmFnFg(Dictionary<string, string?> args, IDocument document)
	{
		var el = document.CreateElement("span");

		if (args.TryGetValue("color", out var color) && ValidColor(color))
			el.SetAttribute("style", $"display: inline-block; color: #{color};");

		return el;
	}
	
	private static INode MfmFnBg(Dictionary<string, string?> args, IDocument document)
	{
		var el = document.CreateElement("span");

		if (args.TryGetValue("color", out var color) && ValidColor(color))
			el.SetAttribute("style", $"display: inline-block; background-color: #{color};");

		return el;
	}
	
	private static INode MfmFnBorder(Dictionary<string, string?> args, IDocument document)
	{
		var el = document.CreateElement("span");

		var width  = args.GetValueOrDefault("width") ?? "1";
		var radius = args.GetValueOrDefault("radius") ?? "0";
		var style  = args.GetValueOrDefault("style") ?? "solid";
		var color  = args.TryGetValue("color", out var c) && ValidColor(c) ? "#" + c : "var(--notice-color)";
		
		el.SetAttribute("style", $"display: inline-block; border: {width}px {style} {color}; border-radius: {radius}px; overflow: clip;");

		return el;
	}

	private static string? GetNodeText(IMfmNode node)
	{
		return node switch
		{
			MfmTextNode mfmTextNode => mfmTextNode.Text,
			_                                    => null,
		};
	}

	private static INode MfmFnRuby(MfmFnNode node, IDocument document)
	{
		var el = document.CreateElement("ruby");

		if (node.Children.Length != 1) return el;
		var childText = GetNodeText(node.Children[0]);
		if (childText == null) return el;
		var split = childText.SplitSpaces();
		if (split.Length < 2) return el;
		
		el.TextContent = split[0];

		var rp1 = document.CreateElement("rp");
		rp1.TextContent = "(";
		el.AppendChild(rp1);

		var rt = document.CreateElement("rt");
		rt.TextContent = split[1];
		el.AppendChild(rt);

		var rp2 = document.CreateElement("rp");
		rp1.TextContent = ")";
		el.AppendChild(rp2);

		return el;
	}

	private static INode MfmFnUnixtime(MfmFnNode node, IDocument document)
	{
		var el = document.CreateElement("time");

		if (node.Children.Length != 1) return el;
		var childText = GetNodeText(node.Children[0]);
		if (childText == null) return el;

		double timestamp;
		if (!double.TryParse(childText, out timestamp)) return el;

		var date = DateTime.UnixEpoch.AddSeconds(timestamp);
		el.SetAttribute("datetime", date.ToLocalTime().ToString("O"));
		el.TextContent = date.ToLocalTime().ToString("G");

		return el;
	}
}