using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using CommonMark;
using CommonMark.Syntax;
using UnityEngine.UI;
using TMPro;
using CustomUI.BeatSaber;
using IPA.Utilities;
using System.Reflection;
using UnityEngine.EventSystems;
using System.Diagnostics;
using System.Collections;
using System.IO;
using System.Text;
using System.Text.RegularExpressions;

namespace BSIPA_ModList.UI.ViewControllers
{
    /// <summary>
    /// A UI component that renders CommonMark Markdown in-game.
    /// </summary>
    [RequireComponent(typeof(RectTransform))]
    public class MarkdownView : MonoBehaviour
    {
        private class TagTypeComponent : MonoBehaviour
        {
            internal BlockTag Tag;
            internal HeadingData hData;
            internal ListData lData;
            internal int listCount;
            internal int listIndex;
        }

        private string markdown = "";
        private bool mdDirty = false;
        
        /// <summary>
        /// The text to be rendered.
        /// </summary>
        /// <remarks>
        /// When this is assigned, the object is marked dirty. It will re-render on the next Update tick.
        /// </remarks>
        /// <value>the text to render as Markdown</value>
        public string Markdown
        {
            get => markdown;
            set
            {
                markdown = value;
                mdDirty = true;
            }
        }

        /// <summary>
        /// A convenience property to access the <see cref="RectTransform"/> on the <see cref="GameObject"/> this is on.
        /// </summary>
        /// <value>the <see cref="RectTransform"/> associated with this component</value>
        public RectTransform rectTransform => GetComponent<RectTransform>();

        private ScrollView scrView;
        private RectTransform content;
        private RectTransform viewport;

        private CommonMarkSettings settings;

        /// <summary>
        /// Creates a new <see cref="MarkdownView"/>. Should never be called directly. Instead, use <see cref="GameObject.AddComponent{T}"/>.
        /// </summary>
        public MarkdownView()
        {
            settings = CommonMarkSettings.Default.Clone();
            settings.AdditionalFeatures = CommonMarkAdditionalFeatures.All;
            settings.RenderSoftLineBreaksAsLineBreaks = false;
            settings.UriResolver = ResolveUri;
        }

        /// <summary>
        /// This function will be called whenever attempting to resolve an image URI, to ensure that the image exists in the embedded assembly.
        /// </summary>
        /// <value>a delegate for the function to call</value>
        public Func<string, bool> HasEmbeddedImage;

        private string ResolveUri(string arg)
        {
            var name = arg.Substring(3);
            if (!arg.StartsWith("!::") && !arg.StartsWith("w::"))
            { // !:: means embedded, w:: means web
              // this block is for when neither is specified

                Logger.md.Debug($"Resolving nonspecific URI {arg}");
                // check if its embedded
                if (HasEmbeddedImage != null && HasEmbeddedImage(arg))
                    return "!::" + arg;
                else
                    return "w::" + arg;
            }

            Logger.md.Debug($"Resolved specific URI {arg}");
            return arg;
        }

        private static string GetLinkUri(string uri)
        {
            if (uri[0] == '!')
            {
                Logger.md.Error($"Cannot link to embedded resource in mod description");
                return null;
            }
            else
                return uri.Substring(3);
        }

        private static Stream ConsolasAssetBundleFontStream => Assembly.GetExecutingAssembly().GetManifestResourceStream("BSIPA_ModList.Bundles.consolas.font");

        private static AssetBundleCreateRequest _bundleRequest;
        private static AssetBundle _bundle;
        private static AssetBundle Bundle
        {
            get
            {
                if (_bundle == null && _bundleRequest != null)
                    throw new InvalidOperationException("Asset bundle is being loaded asynchronously; please wait for that to complete");
                if (_bundle == null)
                    _bundle = AssetBundle.LoadFromStream(ConsolasAssetBundleFontStream);
                return _bundle;
            }
        }

        private static AssetBundleRequest _consolasRequest;
        private static TMP_FontAsset _unsetConsolas;
        private static TMP_FontAsset _consolas;
        private static TMP_FontAsset Consolas
        {
            get
            {
                if (_unsetConsolas == null && _consolasRequest != null)
                    throw new InvalidOperationException("Asset is being loaded asynchronously; please wait for that to complete");
                if (_unsetConsolas == null)
                    _unsetConsolas = Bundle?.LoadAsset<TMP_FontAsset>("CONSOLAS");
                if (_consolas == null && _unsetConsolas != null)
                    _consolas = SetupFont(_unsetConsolas);
                return _consolas;
            }
        }

        private static TMP_FontAsset SetupFont(TMP_FontAsset f)
        {
            var originalFont = Resources.FindObjectsOfTypeAll<TMP_FontAsset>().Last(f2 => f2.name == "Teko-Medium SDF No Glow");
            var matCopy = Instantiate(originalFont.material);
            matCopy.mainTexture = f.material.mainTexture;
            matCopy.mainTextureOffset = f.material.mainTextureOffset;
            matCopy.mainTextureScale = f.material.mainTextureScale;
            f.material = matCopy;
            f = Instantiate(f);
            MaterialReferenceManager.AddFontAsset(f);
            return f;
        }

        internal static void StartLoadResourcesAsync()
        {
            SharedCoroutineStarter.instance.StartCoroutine(LoadResourcesAsync());
        }
        private static IEnumerator LoadResourcesAsync()
        {
            Logger.md.Debug("Starting to load resources");

            _bundleRequest = AssetBundle.LoadFromStreamAsync(ConsolasAssetBundleFontStream);
            yield return _bundleRequest;
            _bundle = _bundleRequest.assetBundle;

            Logger.md.Debug("Bundle loaded");

            _consolasRequest = _bundle.LoadAssetAsync<TMP_FontAsset>("CONSOLAS");
            yield return _consolasRequest;
            _unsetConsolas = _consolasRequest.asset as TMP_FontAsset;

            Logger.md.Debug("Font loaded");
        }

        internal void Awake()
        {
            if (Consolas == null)
                Logger.md.Error($"Loading of Consolas font failed");

            gameObject.SetActive(false);

            var vpgo = new GameObject("Viewport");
            viewport = vpgo.AddComponent<RectTransform>();
            viewport.SetParent(transform);
            viewport.localPosition = Vector2.zero;
            viewport.anchorMin = Vector2.zero;
            viewport.anchorMax = Vector2.one;
            viewport.anchoredPosition = new Vector2(.5f, .5f);
            viewport.sizeDelta = Vector2.zero;
            var vpmask = vpgo.AddComponent<Mask>();
            var vpim = vpgo.AddComponent<Image>(); // supposedly Mask needs an Image?
            vpmask.showMaskGraphic = false;
            vpim.color = Color.white;
            vpim.sprite = WhitePixel;
            vpim.material = CustomUI.Utilities.UIUtilities.NoGlowMaterial;

            content = new GameObject("Content Wrapper").AddComponent<RectTransform>();
            content.SetParent(viewport);
            content.gameObject.AddComponent<TagTypeComponent>();
            content.localPosition = Vector2.zero;
            content.anchorMin = new Vector2(0f, 1f);
            content.anchorMax = new Vector2(1f, 1f);
            content.anchoredPosition = Vector2.zero;
            var contentLayout = content.gameObject.AddComponent<VerticalLayoutGroup>();
            var contentFitter = content.gameObject.AddComponent<ContentSizeFitter>();
            contentFitter.horizontalFit = ContentSizeFitter.FitMode.PreferredSize;
            contentFitter.verticalFit = ContentSizeFitter.FitMode.PreferredSize;
            contentLayout.childControlHeight = true;
            contentLayout.childControlWidth = false;
            contentLayout.childForceExpandHeight = false;
            contentLayout.childForceExpandWidth = true;
            contentLayout.childAlignment = TextAnchor.UpperCenter;
            contentLayout.spacing = 0f;

            var pageUp = Instantiate(Resources.FindObjectsOfTypeAll<Button>().Last((Button x) => x.name == "PageUpButton"), rectTransform, false);
            var pageDown = Instantiate(Resources.FindObjectsOfTypeAll<Button>().Last((Button x) => x.name == "PageDownButton"), rectTransform, false);

            {
                var pup_rt = pageUp.transform as RectTransform;
                var pup_sof = pup_rt.sizeDelta.y;
                var pup_xoff = (rectTransform.sizeDelta.x / 2) + (pup_sof / 2);
                pup_rt.anchoredPosition = new Vector2(pup_xoff, pup_rt.anchoredPosition.y);
                var pup_bg_rt = pup_rt.Find("BG") as RectTransform;
                pup_bg_rt.sizeDelta = new Vector2(pup_bg_rt.sizeDelta.y, pup_bg_rt.sizeDelta.y);

                // fix hitbox
                pup_rt.anchorMin = new Vector2(.5f, pup_rt.anchorMin.y);
                pup_rt.anchorMax = new Vector2(.5f, pup_rt.anchorMax.y);
                pup_rt.sizeDelta = new Vector2(pup_rt.sizeDelta.y, pup_rt.sizeDelta.y);
            }
            {
                var pdn_rt = pageDown.transform as RectTransform;
                var pdn_sof = pdn_rt.sizeDelta.y;
                var pdn_xoff = (rectTransform.sizeDelta.x / 2) + (pdn_sof / 2);
                pdn_rt.anchoredPosition = new Vector2(pdn_xoff, pdn_rt.anchoredPosition.y);
                var pdn_bg_rt = pdn_rt.Find("BG") as RectTransform;
                pdn_bg_rt.sizeDelta = new Vector2(pdn_bg_rt.sizeDelta.y, pdn_bg_rt.sizeDelta.y);

                // fix hitbox
                pdn_rt.anchorMin = new Vector2(.5f, pdn_rt.anchorMin.y);
                pdn_rt.anchorMax = new Vector2(.5f, pdn_rt.anchorMax.y);
                pdn_rt.sizeDelta = new Vector2(pdn_rt.sizeDelta.y, pdn_rt.sizeDelta.y);
            }

            scrView = gameObject.AddComponent<ScrollView>();
            scrView.SetPrivateField("_pageUpButton", pageUp);
            scrView.SetPrivateField("_pageDownButton", pageDown);
            scrView.SetPrivateField("_contentRectTransform", content);
            scrView.SetPrivateField("_viewport", viewport);

            gameObject.SetActive(true);
        }

        private static Sprite whitePixel;
        private static Sprite WhitePixel
        {
            get
            {
                if (whitePixel == null)
                    whitePixel = Resources.FindObjectsOfTypeAll<Sprite>().First(s => s.name == "WhitePixel");
                return whitePixel;
            }
        }

        private static Sprite blockQuoteBackground;
        private static Sprite BlockQuoteBackground
        {
            get
            {
                if (blockQuoteBackground == null)
                    blockQuoteBackground = Resources.FindObjectsOfTypeAll<Sprite>().First(s => s.name == "RoundRectNormal");
                return blockQuoteBackground;
            }
        }

        private static readonly Color BlockQuoteColor = new Color( 30f / 255, 109f / 255, 178f / 255, .25f);
        private static readonly Color BlockCodeColor  = new Color(135f / 255, 135f / 255, 135f / 255, .5f);

#if DEBUG
#if UI_CONFIGURE_MARKDOWN_THEMATIC_BREAK
        private byte tbreakSettings = 0;
#endif
#endif
        internal void Update()
        {
#if DEBUG && UI_CONFIGURE_MARKDOWN_THEMATIC_BREAK
            if (Input.GetKeyDown(KeyCode.K))
            {
                tbreakSettings = (byte)((tbreakSettings + 1) % 16);
                UpdateMd();
                Logger.md.Info(tbreakSettings.ToString());
            }
#endif
            if (mdDirty)
                StartCoroutine(UpdateMd());
        }

        [Flags]
        private enum CurrentTextFlags
        {
            None = 0, Bold = 1, Italic = 2, Underline = 4, Strikethrough = 8,
        }

        private const string LinkDefaultColor = "#59B0F4";
        // private const string LinkHoverColor = "#009dff";

        private static readonly Regex linkRefTitleColorRegex = new Regex(@"\{\s*color:\s*#?([a-fA-F0-9]{6})\s*\}", RegexOptions.Compiled | RegexOptions.Singleline);

        const float PSize = 3.5f;
        const float BlockCodeSize = PSize - .5f;
        const float H1Size = 5.5f;
        const float HLevelDecrease = 0.5f;

        private IEnumerator UpdateMd()
        {
            mdDirty = false;
            Clear();

            // enable so it will set stuff up
            content.gameObject.GetComponent<VerticalLayoutGroup>().enabled = true;

            var doc = CommonMarkConverter.Parse(markdown, settings);

            Stack<RectTransform> layout = new Stack<RectTransform>();
            layout.Push(content);
            TextMeshProUGUI currentText = null;
            List<TextMeshProUGUI> texts = new List<TextMeshProUGUI>();
            CurrentTextFlags textFlags = 0;
            foreach (var node in doc.AsEnumerable())
            {
                Logger.md.Debug($"node {node}");

                if (node.Block != null)
                {
                    var block = node.Block;

                    const float BreakHeight = .5f;
                    const int TextInset = 1;
                    const int BlockQuoteInset = TextInset * 2;
                    const int BlockCodeInset = BlockQuoteInset;
                    const int ListInset = TextInset;

                    void Spacer(float size = 1.5f)
                    {
                        var go = new GameObject("Spacer", typeof(RectTransform));
                        var vlayout = go.GetComponent<RectTransform>();
                        vlayout.SetParent(layout.Peek());
                        vlayout.anchorMin = new Vector2(.5f, .5f);
                        vlayout.anchorMax = new Vector2(.5f, .5f);
                        vlayout.localScale = Vector3.one;
                        vlayout.localPosition = Vector3.zero;

                        var l = go.AddComponent<LayoutElement>();
                        l.minHeight = l.preferredHeight  = size;
                    }

                    HorizontalOrVerticalLayoutGroup BlockNode(string name, float spacing, bool isVertical, Action<TagTypeComponent> apply = null, float? spacer = null, bool isDoc = false, bool matchWidth = false, float matchWidthDiff = 0f)
                    {
                        if (node.IsOpening)
                        {
                            Logger.md.Debug($"Creating block container {name}");

                            currentText = null;
                            var go = new GameObject(name, typeof(RectTransform));
                            var vlayout = go.GetComponent<RectTransform>();
                            vlayout.SetParent(layout.Peek());
                            //vlayout.anchoredPosition = new Vector2(.5f, .5f);
                            vlayout.anchorMin = new Vector2(.5f, .5f);
                            vlayout.anchorMax = new Vector2(.5f, .5f);
                            vlayout.localScale = Vector3.one;
                            vlayout.localPosition = Vector3.zero;
                            if (isDoc)
                            {
                                vlayout.sizeDelta = new Vector2(rectTransform.rect.width, 0f);
                                vlayout.anchorMin = new Vector2(0f, 1f);
                                vlayout.anchorMax = new Vector2(1f, 1f);
                            }
                            if (matchWidth)
                                vlayout.sizeDelta = new Vector2(layout.Peek().rect.width-matchWidthDiff, 0f);

                            var tt = go.AddComponent<TagTypeComponent>();
                            tt.Tag = block.Tag;
                            apply?.Invoke(tt);
                            layout.Push(vlayout);

                            HorizontalOrVerticalLayoutGroup l;
                            if (isVertical)
                                l = go.AddComponent<VerticalLayoutGroup>();
                            else
                                l = go.AddComponent<HorizontalLayoutGroup>();

                            l.childControlHeight = l.childControlWidth = true;
                            l.childForceExpandHeight = false;
                            l.childForceExpandWidth = isDoc;
                            l.spacing = spacing;

                            if (isDoc)
                                vlayout.anchoredPosition = new Vector2(0f, -vlayout.rect.height);

                            return l;
                        }
                        else if (node.IsClosing)
                        {
                            currentText = null;
                            layout.Pop();

                            if (spacer.HasValue)
                                Spacer(spacer.Value);
                        }
                        return null;
                    }

                    void ThematicBreak(float spacerSize = 1.5f)
                    { // TODO: Fix positioning
                        var go = new GameObject("ThematicBreak", typeof(RectTransform), typeof(HorizontalLayoutGroup));
                        var vlayout = go.GetComponent<RectTransform>();
                        vlayout.SetParent(layout.Peek());
                        var l = go.GetComponent<HorizontalLayoutGroup>();
#if DEBUG && UI_CONFIGURE_MARKDOWN_THEMATIC_BREAK
                        l.childControlHeight = (tbreakSettings & 0b0001) != 0; // if set, not well behaved
                        l.childControlWidth = (tbreakSettings & 0b0010) != 0;
                        l.childForceExpandHeight = (tbreakSettings & 0b0100) != 0; // if set, not well behaved
                        l.childForceExpandWidth = (tbreakSettings & 0b1000) != 0;
#else
                        l.childControlHeight = false;
                        l.childControlWidth = false;
                        l.childForceExpandHeight = false;
                        l.childForceExpandWidth = false;
#endif
                        l.childAlignment = TextAnchor.UpperCenter;
                        l.spacing = 0f;

                        vlayout.localScale = Vector3.one;
                        vlayout.anchoredPosition = Vector2.zero;
                        vlayout.anchorMin = new Vector2(.5f, .5f);
                        vlayout.anchorMax = new Vector2(.5f, .5f);
                        vlayout.sizeDelta = new Vector2(layout.Peek().rect.width, BreakHeight);
                        vlayout.localPosition = Vector3.zero;

                        currentText = null;
                        go = new GameObject("ThematicBreak Bar", typeof(RectTransform), typeof(Image), typeof(LayoutElement));
                        var im = go.GetComponent<Image>();
                        im.color = Color.white;
                        // i think i need to copy the sprite because i'm using the same one for the mask
                        im.sprite = Sprite.Create(WhitePixel.texture, WhitePixel.rect, Vector2.zero);
                        im.material = CustomUI.Utilities.UIUtilities.NoGlowMaterial;
                        var rt = go.GetComponent<RectTransform>();
                        rt.SetParent(vlayout);
                        var le = go.GetComponent<LayoutElement>();
                        le.minWidth = le.preferredWidth = layout.Peek().rect.width;
                        le.minHeight = le.preferredHeight = BreakHeight;
                        le.flexibleHeight = le.flexibleWidth = 1f;
                        rt.localScale = Vector3.one;
                        rt.localPosition = Vector3.zero;
                        rt.anchoredPosition = Vector3.zero;
                        rt.anchorMin = Vector2.zero;
                        rt.anchorMax = Vector2.one;
                        rt.sizeDelta = new Vector2(layout.Peek().rect.width, BreakHeight);

                        if (spacerSize != 0f)
                            Spacer(spacerSize);
                    }

                    switch (block.Tag)
                    {
                        case BlockTag.Document:
                            BlockNode("DocumentRoot", .5f, true, isDoc: true);
                            break;
                        case BlockTag.SetextHeading:
                            var l = BlockNode("SeHeading", 0f, false, t => t.hData = block.Heading, spacer: 0f);
                            if (l)
                            {
                                l.childAlignment = TextAnchor.UpperCenter;
                                l.padding = new RectOffset(TextInset, TextInset, 0, 0);
                            }
                            else
                                ThematicBreak(2f);
                            break;
                        case BlockTag.AtxHeading:
                                l = BlockNode("AtxHeading", .1f, false, t => t.hData = block.Heading);
                            if (l) l.padding = new RectOffset(TextInset, TextInset, 0, 0);
                            if (l && block.Heading.Level == 1)
                                l.childAlignment = TextAnchor.UpperCenter;
                            break;
                        case BlockTag.Paragraph:
                                l = BlockNode("Paragraph", .1f, false, spacer: 1.5f);
                            if (l) l.padding = new RectOffset(TextInset, TextInset, 0, 0);
                            break;
                        case BlockTag.ThematicBreak:
                            ThematicBreak();
                            break;
                        // TODO: add the rest of the tag types
                        case BlockTag.BlockQuote:
                            l = BlockNode("BlockQuote", .1f, true, matchWidth: true, matchWidthDiff: BlockQuoteInset*2, spacer: 1.5f);
                            if (l)
                            {
                                l.childForceExpandWidth = true;
                                l.padding = new RectOffset(BlockQuoteInset, BlockQuoteInset, BlockQuoteInset, 0);
                                var go = l.gameObject;

                                var im = go.AddComponent<Image>();
                                im.material = CustomUI.Utilities.UIUtilities.NoGlowMaterial;
                                im.type = Image.Type.Sliced;
                                im.sprite = Instantiate(BlockQuoteBackground);
                                im.color = BlockQuoteColor;
                            }
                            break;
                        case BlockTag.IndentedCode:
                        case BlockTag.FencedCode:
                            {
                                currentText = null;
                                var go = new GameObject("CodeBlock", typeof(RectTransform));
                                var vlayout = go.GetComponent<RectTransform>();
                                vlayout.SetParent(layout.Peek());

                                vlayout.anchorMin = new Vector2(.5f, .5f);
                                vlayout.anchorMax = new Vector2(.5f, .5f);
                                vlayout.localScale = Vector3.one;
                                vlayout.localPosition = Vector3.zero;
                                vlayout.sizeDelta = new Vector2(layout.Peek().rect.width - BlockCodeInset * 2, 0f);

                                var tt = go.AddComponent<TagTypeComponent>();
                                tt.Tag = block.Tag;

                                l = go.AddComponent<VerticalLayoutGroup>();

                                l.childControlHeight = l.childControlWidth = true;
                                l.childForceExpandHeight = false;
                                l.childForceExpandWidth = true;
                                l.spacing = 1.5f;
                                l.padding = new RectOffset(BlockCodeInset, BlockCodeInset, BlockCodeInset, BlockCodeInset);

                                var im = go.AddComponent<Image>();
                                im.material = CustomUI.Utilities.UIUtilities.NoGlowMaterial;
                                im.type = Image.Type.Sliced;
                                im.sprite = Instantiate(BlockQuoteBackground);
                                im.color = BlockCodeColor;

                                var text = BeatSaberUI.CreateText(vlayout, $"<noparse>{block.StringContent}</noparse>", Vector2.zero);
                                text.fontSize = BlockCodeSize;
                                text.font = Consolas;
                            }
                            break;

                        case BlockTag.List:
                            l = BlockNode("List", .05f, true, t => t.lData = block.ListData, matchWidth: true, matchWidthDiff: ListInset * 2, spacer: 1.5f);
                            if (l)
                            {
                                l.childForceExpandWidth = true;
                                l.padding = new RectOffset(ListInset, ListInset, 0, 0);
                                var go = l.gameObject;
                                var tt = go.GetComponent<TagTypeComponent>();

                                // count up children
                                var count = 0;
                                for (var c = block.FirstChild; c != null; c = c.NextSibling) count++;
                                tt.listCount = count;
                                tt.listIndex = 0;
                            }
                            break;
                        case BlockTag.ListItem:
                            l = BlockNode("ListItem", .05f, false, matchWidth: true, spacer: null);
                            if (l)
                            { // TODO: this is mega scuffed
                                l.childForceExpandWidth = true;
                                var go = l.gameObject;
                                var rt = go.GetComponent<RectTransform>();
                                var tt = go.GetComponent<TagTypeComponent>();
                                var ptt = rt.parent.gameObject.GetComponent<TagTypeComponent>();

                                var index = ptt.listIndex++;

                                var listCount = ptt.listCount;
                                var maxNum = listCount + ptt.lData.Start;
                                var numChars = (int)Math.Floor(Math.Log10(maxNum) + 1);

                                var cNum = index + ptt.lData.Start;

                                var lt = ptt.lData.ListType;

                                var id = lt == ListType.Bullet ? ptt.lData.BulletChar.ToString() : (cNum + (ptt.lData.Delimiter == ListDelimiter.Parenthesis ? ")" : "."));
                                var ident = BeatSaberUI.CreateText(rt, $"<nobr>{id} </nobr>\n", Vector2.zero);
                                if (lt == ListType.Ordered) // pad it out to fill space
                                    ident.text += $"<nobr><mspace=1em>{new string(' ', numChars + 1)}</mspace></nobr>";

                                var contGo = new GameObject("Content", typeof(RectTransform));
                                var vlayout = contGo.GetComponent<RectTransform>();
                                vlayout.SetParent(rt);

                                vlayout.anchorMin = new Vector2(.5f, .5f);
                                vlayout.anchorMax = new Vector2(.5f, .5f);
                                vlayout.localScale = Vector3.one;
                                vlayout.localPosition = Vector3.zero;
                                //vlayout.sizeDelta = new Vector2(rt.rect.width, 0f);

                                var tt2 = contGo.AddComponent<TagTypeComponent>();
                                tt2.Tag = block.Tag;

                                var csf = contGo.AddComponent<ContentSizeFitter>();
                                csf.horizontalFit = ContentSizeFitter.FitMode.PreferredSize;
                                csf.verticalFit = ContentSizeFitter.FitMode.Unconstrained;

                                l = contGo.AddComponent<VerticalLayoutGroup>();
                                l.childAlignment = TextAnchor.UpperLeft;
                                l.childControlHeight = l.childControlWidth = true;
                                l.childForceExpandHeight = false;
                                l.childForceExpandWidth = true;
                                l.spacing = .5f;

                                layout.Push(vlayout);
                            }
                            else
                                layout.Pop(); // pop one more to clear content rect from stack
                            break;

                        case BlockTag.HtmlBlock:
                            break;

                        case BlockTag.ReferenceDefinition: // i have no idea what the state looks like here
                            break;
                    }
                }
                else if (node.Inline != null)
                { // inline element
                    var inl = node.Inline;

                    void Flag(CurrentTextFlags flag)
                    {
                        if (node.IsOpening)
                            textFlags |= flag;
                        else if (node.IsClosing)
                            textFlags &= ~flag;
                    }

                    void EnsureText()
                    {
                        if (currentText == null)
                        {
                            Logger.md.Debug($"Adding new text element");

                            var tt = layout.Peek().gameObject.GetComponent<TagTypeComponent>();
                            currentText = BeatSaberUI.CreateText(layout.Peek(), "", Vector2.zero);
                            currentText.gameObject.AddComponent<TextLinkDecoder>();

                            currentText.enableWordWrapping = true;

                            switch (tt.Tag)
                            {
                                case BlockTag.List:
                                case BlockTag.ListItem:
                                case BlockTag.Paragraph:
                                    currentText.fontSize = PSize;
                                    break;
                                case BlockTag.AtxHeading:
                                    var size = H1Size;
                                    size -= HLevelDecrease * (tt.hData.Level - 1);
                                    currentText.fontSize = size;
                                    if (tt.hData.Level == 1)
                                        currentText.alignment = TextAlignmentOptions.Center;
                                    break;
                                case BlockTag.SetextHeading:
                                    currentText.fontSize = H1Size;
                                    currentText.alignment = TextAlignmentOptions.Center;
                                    break;
                                    // TODO: add other relevant types
                            }

                            texts.Add(currentText);
                        }
                    }
                    switch (inl.Tag)
                    {
                        case InlineTag.String:
                            EnsureText();

                            string head = "<noparse>", tail = "</noparse>";
                            if (textFlags.HasFlag(CurrentTextFlags.Bold))
                            { head = "<b>" + head; tail += "</b>"; }
                            if (textFlags.HasFlag(CurrentTextFlags.Italic))
                            { head = "<i>" + head; tail += "</i>"; }
                            if (textFlags.HasFlag(CurrentTextFlags.Strikethrough))
                            { head = "<s>" + head; tail += "</s>"; }
                            if (textFlags.HasFlag(CurrentTextFlags.Underline))
                            { head = "<u>" + head; tail += "</u>"; }

                            currentText.text += head + inl.LiteralContent + tail;
                            break;
                        case InlineTag.Strong:
                            Flag(CurrentTextFlags.Bold);
                            break;
                        case InlineTag.Strikethrough:
                            Flag(CurrentTextFlags.Strikethrough);
                            break;
                        case InlineTag.Emphasis:
                            Flag(CurrentTextFlags.Italic);
                            break;
                        case InlineTag.Code:
                            EnsureText();
                            currentText.text += $"<font=\"CONSOLAS\"><size=80%><mark=#A0A0C080><noparse>{inl.LiteralContent}</noparse></mark></size></font>";
                            break;
                        case InlineTag.SoftBreak:
                            EnsureText();
                            currentText.text += " "; // soft breaks translate to a space
                            break;
                        case InlineTag.LineBreak:
                            EnsureText();
                            currentText.text += "\n"; // line breaks translate to a new line
                            break;
                        case InlineTag.Link:
                            EnsureText();
                            Flag(CurrentTextFlags.Underline);

                            var color = LinkDefaultColor;
                            var targetUrl = ResolveUri(inl.TargetUrl);

                            var m = linkRefTitleColorRegex.Match(inl.LiteralContent);
                            if (m.Success)
                                color = "#" + m.Groups[1].Value;

                            if (node.IsOpening)
                                currentText.text += $"<color={color}><link=\"{targetUrl}\">";
                            else if (node.IsClosing)
                                currentText.text += "</link></color>";
                            break;
                        case InlineTag.RawHtml:
                            EnsureText();
                            currentText.text += inl.LiteralContent;
                            break;
                        case InlineTag.Placeholder:

                            break;
                    }
                }
            }

            yield return null; // delay one frame
             
            scrView.Setup();

            // this is the bullshit I have to use to make it work properly
            content.gameObject.GetComponent<VerticalLayoutGroup>().enabled = false;
            var childRt = content.GetChild(0) as RectTransform;
            childRt.anchoredPosition = new Vector2(0f, childRt.anchoredPosition.y);
        }

        private class TextLinkDecoder : MonoBehaviour, IPointerClickHandler
        {
            private TextMeshProUGUI tmp;

            public void Awake()
            {
                tmp = GetComponent<TextMeshProUGUI>();
            }

            public void OnPointerClick(PointerEventData eventData)
            {
                // this may not actually get me what i want
                int linkIndex = TMP_TextUtilities.FindIntersectingLink(tmp, eventData.pointerPressRaycast.worldPosition, null);
                if (linkIndex != -1)
                { // was a link clicked?
                    TMP_LinkInfo linkInfo = tmp.textInfo.linkInfo[linkIndex];

                    // open the link id as a url, which is the metadata we added in the text field
                    var qualifiedUrl = linkInfo.GetLinkID();
                    if (qualifiedUrl.StartsWith("$$"))
                        return; // this means its used for something else

                    Logger.md.Debug($"Link pressed {qualifiedUrl}");

                    var uri = GetLinkUri(qualifiedUrl);
                    if (uri != null)
                        Process.Start(uri);
                }
            }

            private List<Color32[]> SetLinkToColor(int linkIndex, Color32 color)
            {
                TMP_LinkInfo linkInfo = tmp.textInfo.linkInfo[linkIndex];

                var oldVertColors = new List<Color32[]>(); // store the old character colors

                for (int i = 0; i < linkInfo.linkTextLength; i++)
                { // for each character in the link string
                    int characterIndex = linkInfo.linkTextfirstCharacterIndex + i; // the character index into the entire text
                    var charInfo = tmp.textInfo.characterInfo[characterIndex];
                    int meshIndex = charInfo.materialReferenceIndex; // Get the index of the material / sub text object used by this character.
                    int vertexIndex = charInfo.vertexIndex; // Get the index of the first vertex of this character.

                    Color32[] vertexColors = tmp.textInfo.meshInfo[meshIndex].colors32; // the colors for this character
                    oldVertColors.Add(vertexColors.ToArray());

                    if (charInfo.isVisible)
                    {
                        vertexColors[vertexIndex + 0] = color;
                        vertexColors[vertexIndex + 1] = color;
                        vertexColors[vertexIndex + 2] = color;
                        vertexColors[vertexIndex + 3] = color;
                    }
                }

                // Update Geometry
                tmp.UpdateVertexData(TMP_VertexDataUpdateFlags.All);

                return oldVertColors;
            }
        }

        private void Clear()
        {
            content.gameObject.SetActive(false);
            void Clear(Transform target)
            {
                foreach (Transform child in target)
                {
                    Clear(child);
                    Logger.md.Debug($"Destroying {child.name}");
                    child.SetParent(null);
                    Destroy(child.gameObject);
                }
            }
            Clear(content);
            content.gameObject.SetActive(true);
        }
    }
}
