using Dalamud.Bindings.ImGui;
using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Interface;
using Dalamud.Interface.ImGuiSeStringRenderer;
using Dalamud.Interface.Textures.TextureWraps;
using Dalamud.Interface.Utility;
using Lumina.Text.Parse;
using Lumina.Text.ReadOnly;
using System.Globalization;
using System.Numerics;
using System.Reflection;
using System.Text;
using DalamudSeString = Dalamud.Game.Text.SeStringHandling.SeString;
using DalamudSeStringBuilder = Dalamud.Game.Text.SeStringHandling.SeStringBuilder;
using LuminaSeStringBuilder = Lumina.Text.SeStringBuilder;

namespace LightlessSync.Utils;

public static class SeStringUtils
{
    private static int _seStringHitboxCounter;
    private static int _iconHitboxCounter;

    public static bool TryRenderSeStringMarkupAtCursor(string payload)
    {
        if (string.IsNullOrWhiteSpace(payload))
            return false;

        var wrapWidth = ImGui.GetContentRegionAvail().X;
        if (wrapWidth <= 0f || float.IsNaN(wrapWidth) || float.IsInfinity(wrapWidth))
            wrapWidth = float.MaxValue;

        var normalizedPayload = payload.ReplaceLineEndings("<br>");
        try
        {
            _ = ReadOnlySeString.FromMacroString(normalizedPayload, new MacroStringParseOptions
            {
                ExceptionMode = MacroStringParseExceptionMode.Throw
            });
        }
        catch (Exception)
        {
            return false;
        }

        try
        {
            var drawParams = new SeStringDrawParams
            {
                WrapWidth = wrapWidth,
                Font = ImGui.GetFont(),
                Color = ImGui.GetColorU32(ImGuiCol.Text),
            };

            var renderId = ImGui.GetID($"SeStringMarkup##{normalizedPayload.GetHashCode(StringComparison.Ordinal)}");
            var drawResult = ImGuiHelpers.CompileSeStringWrapped(normalizedPayload, drawParams, renderId);
            var height = drawResult.Size.Y;
            if (height <= 0f)
                height = ImGui.GetTextLineHeight();

            ImGui.Dummy(new Vector2(0f, height));

            if (drawResult.InteractedPayloadEnvelope.Length > 0 &&
                TryExtractLink(drawResult.InteractedPayloadEnvelope, drawResult.InteractedPayload, out var linkUrl, out var tooltipText))
            {
                var hoverText = !string.IsNullOrEmpty(linkUrl) ? linkUrl : tooltipText;

                if (!string.IsNullOrEmpty(hoverText))
                {
                    ImGui.BeginTooltip();
                    ImGui.TextUnformatted(hoverText);
                    ImGui.EndTooltip();
                }

                if (!string.IsNullOrEmpty(linkUrl))
                    ImGui.SetMouseCursor(ImGuiMouseCursor.Hand);

                if (drawResult.Clicked && !string.IsNullOrEmpty(linkUrl))
                    Dalamud.Utility.Util.OpenLink(linkUrl);
            }

            return true;
        }
        catch (Exception ex)
        {
            ImGui.TextDisabled($"[SeString error] {ex.Message}");
            return false;
        }
    }

    public enum SeStringSegmentType
    {
        Text,
        Icon
    }

    public readonly record struct SeStringSegment(
        SeStringSegmentType Type,
        string? Text,
        Vector4? Color,
        uint IconId,
        IDalamudTextureWrap? Texture,
        Vector2 Size);

    public static bool TryResolveSegments(
        string payload,
        float scale,
        Func<uint, IDalamudTextureWrap?> iconResolver,
        List<SeStringSegment> resolvedSegments,
        out Vector2 totalSize)
    {
        totalSize = Vector2.Zero;
        if (string.IsNullOrWhiteSpace(payload))
            return false;

        var parsedSegments = new List<ParsedSegment>(Math.Max(1, payload.Length / 4));
        if (!ParseSegments(payload, parsedSegments))
            return false;

        float width = 0f;
        float height = 0f;

        foreach (var segment in parsedSegments)
        {
            switch (segment.Type)
            {
                case ParsedSegmentType.Text:
                {
                    var text = segment.Text ?? string.Empty;
                    if (text.Length == 0)
                        break;

                    var textSize = ImGui.CalcTextSize(text);
                    resolvedSegments.Add(new SeStringSegment(SeStringSegmentType.Text, text, segment.Color, 0, null, textSize));
                    width += textSize.X;
                    height = MathF.Max(height, textSize.Y);
                    break;
                }
                case ParsedSegmentType.Icon:
                {
                    var wrap = iconResolver(segment.IconId);
                    Vector2 iconSize;
                    string? fallback = null;
                    if (wrap != null)
                    {
                        iconSize = CalculateIconSize(wrap, scale);
                    }
                    else
                    {
                        fallback = $"[{segment.IconId}]";
                        iconSize = ImGui.CalcTextSize(fallback);
                    }

                    resolvedSegments.Add(new SeStringSegment(SeStringSegmentType.Icon, fallback, segment.Color, segment.IconId, wrap, iconSize));
                    width += iconSize.X;
                    height = MathF.Max(height, iconSize.Y);
                    break;
                }
            }
        }

        totalSize = new Vector2(width, height);
        parsedSegments.Clear();
        return resolvedSegments.Count > 0;
    }

    private enum ParsedSegmentType
    {
        Text,
        Icon
    }

    private readonly record struct ParsedSegment(
        ParsedSegmentType Type,
        string? Text,
        uint IconId,
        Vector4? Color);

    private static bool ParseSegments(string payload, List<ParsedSegment> segments)
    {
        var builder = new StringBuilder(payload.Length);
        Vector4? activeColor = null;
        var index = 0;
        while (index < payload.Length)
        {
            if (payload[index] == '<')
            {
                var end = payload.IndexOf('>', index);
                if (end == -1)
                    break;

                var tagContent = payload.Substring(index + 1, end - index - 1);
                if (TryHandleIconTag(tagContent, segments, builder, activeColor))
                {
                    index = end + 1;
                    continue;
                }

                if (TryHandleColorTag(tagContent, segments, builder, ref activeColor))
                {
                    index = end + 1;
                    continue;
                }

                builder.Append('<');
                builder.Append(tagContent);
                builder.Append('>');
                index = end + 1;
            }
            else
            {
                builder.Append(payload[index]);
                index++;
            }
        }

        if (index < payload.Length)
            builder.Append(payload, index, payload.Length - index);

        FlushTextBuilder(builder, activeColor, segments);
        return segments.Count > 0;
    }

    private static bool TryHandleIconTag(string tagContent, List<ParsedSegment> segments, StringBuilder textBuilder, Vector4? activeColor)
    {
        if (!tagContent.StartsWith("icon(", StringComparison.OrdinalIgnoreCase) || !tagContent.EndsWith(')'))
            return false;

        var inner = tagContent.AsSpan(5, tagContent.Length - 6).Trim();
        if (!uint.TryParse(inner, NumberStyles.Integer, CultureInfo.InvariantCulture, out var iconId))
            return false;

        FlushTextBuilder(textBuilder, activeColor, segments);
        segments.Add(new ParsedSegment(ParsedSegmentType.Icon, null, iconId, null));
        return true;
    }

    private static bool TryHandleColorTag(string tagContent, List<ParsedSegment> segments, StringBuilder textBuilder, ref Vector4? activeColor)
    {
        if (tagContent.StartsWith("color", StringComparison.OrdinalIgnoreCase))
        {
            var equalsIndex = tagContent.IndexOf('=');
            if (equalsIndex == -1)
                return false;

            var value = tagContent.Substring(equalsIndex + 1).Trim().Trim('"');
            if (!TryParseColor(value, out var color))
                return false;

            FlushTextBuilder(textBuilder, activeColor, segments);
            activeColor = color;
            return true;
        }

        if (tagContent.Equals("/color", StringComparison.OrdinalIgnoreCase))
        {
            FlushTextBuilder(textBuilder, activeColor, segments);
            activeColor = null;
            return true;
        }

        return false;
    }

    private static void FlushTextBuilder(StringBuilder builder, Vector4? color, List<ParsedSegment> segments)
    {
        if (builder.Length == 0)
            return;

        segments.Add(new ParsedSegment(ParsedSegmentType.Text, builder.ToString(), 0, color));
        builder.Clear();
    }

    private static bool TryExtractLink(ReadOnlySpan<byte> envelope, Payload? payload, out string url, out string? tooltipText)
    {
        url = string.Empty;
        tooltipText = null;

        if (envelope.Length == 0 && payload is null)
            return false;

        tooltipText = envelope.Length > 0 ? DalamudSeString.Parse(envelope.ToArray()).TextValue : null;

        if (payload is not null && TryReadUrlFromPayload(payload, out url))
            return true;

        if (!string.IsNullOrWhiteSpace(tooltipText))
        {
            var candidate = FindFirstUrl(tooltipText);
            if (!string.IsNullOrEmpty(candidate))
            {
                url = candidate;
                return true;
            }
        }

        url = string.Empty;
        return false;
    }

    private static bool TryReadUrlFromPayload(object payload, out string url)
    {
        url = string.Empty;
        var type = payload.GetType();

        static string? ReadStringProperty(Type type, object instance, string propertyName)
            => type.GetProperty(propertyName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)
                   ?.GetValue(instance) as string;

        string? candidate = ReadStringProperty(type, payload, "Uri")
                            ?? ReadStringProperty(type, payload, "Url")
                            ?? ReadStringProperty(type, payload, "Target")
                            ?? ReadStringProperty(type, payload, "Destination");

        if (IsHttpUrl(candidate))
        {
            url = candidate!;
            return true;
        }

        var dataProperty = type.GetProperty("Data", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
        if (dataProperty?.GetValue(payload) is IEnumerable<string> sequence)
        {
            foreach (var entry in sequence)
            {
                if (IsHttpUrl(entry))
                {
                    url = entry;
                    return true;
                }
            }
        }

        var extraStringProp = type.GetProperty("ExtraString", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
        if (IsHttpUrl(extraStringProp?.GetValue(payload) as string))
        {
            url = (extraStringProp!.GetValue(payload) as string)!;
            return true;
        }

        var textProp = type.GetProperty("Text", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
        if (IsHttpUrl(textProp?.GetValue(payload) as string))
        {
            url = (textProp!.GetValue(payload) as string)!;
            return true;
        }

        return false;
    }

    private static string? FindFirstUrl(string? text)
    {
        if (string.IsNullOrWhiteSpace(text))
            return null;

        var index = text.IndexOf("http", StringComparison.OrdinalIgnoreCase);
        while (index >= 0)
        {
            var end = index;
            while (end < text.Length && !char.IsWhiteSpace(text[end]) && !"\"')]>".Contains(text[end]))
                end++;

            var candidate = text.Substring(index, end - index).TrimEnd('.', ',', ';');
            if (IsHttpUrl(candidate))
                return candidate;

            index = text.IndexOf("http", end, StringComparison.OrdinalIgnoreCase);
        }

        return null;
    }

    private static bool IsHttpUrl(string? value)
    {
        if (string.IsNullOrWhiteSpace(value))
            return false;

        return Uri.TryCreate(value, UriKind.Absolute, out var uri)
               && (string.Equals(uri.Scheme, Uri.UriSchemeHttp, StringComparison.Ordinal) || string.Equals(uri.Scheme, Uri.UriSchemeHttps, StringComparison.Ordinal));
    }

    public static string StripMarkup(string value)
    {
        if (string.IsNullOrEmpty(value))
            return string.Empty;

        var builder = new StringBuilder(value.Length);
        int depth = 0;
        foreach (var c in value)
        {
            if (c == '<')
            {
                depth++;
                continue;
            }

            if (c == '>' && depth > 0)
            {
                depth--;
                continue;
            }

            if (depth == 0)
                builder.Append(c);
        }

        return builder.ToString().Trim();
    }

    private static bool TryParseColor(string value, out Vector4 color)
    {
        color = default;
        if (string.IsNullOrEmpty(value) || value[0] != '#')
            return false;

        var hex = value.AsSpan(1);
        if (!uint.TryParse(hex, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var parsed))
            return false;

        byte a, r, g, b;
        if (hex.Length == 6)
        {
            a = 0xFF;
            r = (byte)(parsed >> 16);
            g = (byte)(parsed >> 8);
            b = (byte)parsed;
        }
        else if (hex.Length == 8)
        {
            a = (byte)(parsed >> 24);
            r = (byte)(parsed >> 16);
            g = (byte)(parsed >> 8);
            b = (byte)parsed;
        }
        else
        {
            return false;
        }

        const float inv = 1.0f / 255f;
        color = new Vector4(r * inv, g * inv, b * inv, a * inv);
        return true;
    }

    private static Vector2 CalculateIconSize(IDalamudTextureWrap wrap, float scale)
    {
        const float IconHeightScale = 1.25f;

        var textHeight = ImGui.GetTextLineHeight();
        var baselineHeight = MathF.Max(1f, textHeight - 2f * scale);
        var targetHeight = MathF.Max(baselineHeight, baselineHeight * IconHeightScale);
        var aspect = wrap.Width > 0 ? wrap.Width / (float)wrap.Height : 1f;
        return new Vector2(targetHeight * aspect, targetHeight);
    }

    public static DalamudSeString BuildFormattedPlayerName(string text, Vector4? textColor, Vector4? glowColor)
    {
        var b = new DalamudSeStringBuilder();

        if (glowColor is Vector4 glow)
            b.Add(new GlowPayload(glow));

        if (textColor is Vector4 color)
            b.Add(new ColorPayload(color));

        b.AddText(text ?? string.Empty);

        if (textColor is not null)
            b.Add(new ColorEndPayload());

        if (glowColor is not null)
            b.Add(new GlowEndPayload());

        return b.Build();
    }

    public static DalamudSeString BuildPlain(string text)
    {
        var b = new DalamudSeStringBuilder();
        b.AddText(text ?? string.Empty);
        return b.Build();
    }

    public static DalamudSeString BuildRichText(ReadOnlySpan<RichTextEntry> fragments)
    {
        var builder = new LuminaSeStringBuilder();

        foreach (var fragment in fragments)
        {
            if (string.IsNullOrEmpty(fragment.Text))
                continue;

            var hasColor = fragment.Color.HasValue;
            if (hasColor)
            {
                Vector4 color = fragment.Color!.Value;
                builder.PushColorRgba(color);
            }

            if (fragment.Bold)
                builder.AppendSetBold(true);

            builder.Append(fragment.Text.AsSpan());

            if (fragment.Bold)
                builder.AppendSetBold(false);

            if (hasColor)
                builder.PopColor();
        }

        return DalamudSeString.Parse(builder.ToArray());
    }

    public static DalamudSeString BuildRichText(params RichTextEntry[] fragments) => BuildRichText(fragments.AsSpan());
    public static void RenderSeString(DalamudSeString seString, Vector2 position, ImFontPtr? font = null, ImDrawListPtr? drawList = null)
    {
        drawList ??= ImGui.GetWindowDrawList();

        var drawParams = new SeStringDrawParams
        {
            Font = font ?? UiBuilder.MonoFont,
            Color = 0xFFFFFFFF,
            WrapWidth = float.MaxValue,
            TargetDrawList = drawList
        };

        ImGui.SetCursorScreenPos(position);
        ImGuiHelpers.SeStringWrapped(seString.Encode(), drawParams);

        var textSize = ImGui.CalcTextSize(seString.TextValue);
        if (textSize.Y <= 0f)
            textSize.Y = ImGui.GetTextLineHeight();

        ImGui.Dummy(new Vector2(0f, textSize.Y));
    }

    public static void RenderSeStringWrapped(DalamudSeString seString, float wrapWidth, ImFontPtr? font = null, ImDrawListPtr? drawList = null)
    {
        drawList ??= ImGui.GetWindowDrawList();

        var usedFont = font ?? ImGui.GetFont();
        var drawParams = new SeStringDrawParams
        {
            Font = usedFont,
            FontSize = usedFont.FontSize,
            Color = ImGui.GetColorU32(ImGuiCol.Text),
            WrapWidth = wrapWidth,
            TargetDrawList = drawList,
            ScreenOffset = ImGui.GetCursorScreenPos()
        };

        ImGuiHelpers.SeStringWrapped(seString.Encode(), drawParams);

        var calcWrapWidth = wrapWidth > 0f ? wrapWidth : -1f;
        var textSize = ImGui.CalcTextSize(seString.TextValue, wrapWidth: calcWrapWidth);
        if (textSize.Y <= 0f)
            textSize.Y = ImGui.GetTextLineHeight();

        ImGui.Dummy(new Vector2(0f, textSize.Y));
    }

    public static Vector2 RenderSeStringWithHitbox(DalamudSeString seString, Vector2 position, ImFontPtr? font = null, string? id = null)
    {
        var drawList = ImGui.GetWindowDrawList();
        var usedFont = font ?? UiBuilder.MonoFont;

        var textSize = ImGui.CalcTextSize(seString.TextValue);
        if (textSize.Y <= 0f)
        {
            textSize.Y = usedFont.FontSize;
        }

        var style = ImGui.GetStyle();
        var fontHeight = usedFont.FontSize > 0f ? usedFont.FontSize : ImGui.GetFontSize();
        var frameHeight = fontHeight + style.FramePadding.Y * 2f;
        var hitboxHeight = MathF.Max(frameHeight, textSize.Y);
        var verticalOffset = MathF.Max((hitboxHeight - textSize.Y) * 0.5f, 0f);

        var drawPos = new Vector2(position.X, position.Y + verticalOffset);
        var drawParams = new SeStringDrawParams
        {
            FontSize = usedFont.FontSize,
            ScreenOffset = drawPos,
            Font = usedFont,
            Color = 0xFFFFFFFF,
            WrapWidth = float.MaxValue,
            TargetDrawList = drawList
        };

        ImGui.SetCursorScreenPos(drawPos);

        ImGuiHelpers.SeStringWrapped(seString.Encode(), drawParams);

        ImGui.SetCursorScreenPos(position);
        if (id is not null)
        {
            ImGui.PushID(id);
        }
        else
        {
            ImGui.PushID(Interlocked.Increment(ref _seStringHitboxCounter));
        }

        try
        {
            ImGui.InvisibleButton("##hitbox", new Vector2(textSize.X, hitboxHeight));
        }
        finally
        {
            ImGui.PopID();
        }

        return new Vector2(textSize.X, hitboxHeight);
    }

    public static Vector2 RenderSeStringWithHitbox(DalamudSeString seString, Vector2 position, float? targetFontSize, ImFontPtr? font = null, string? id = null)
    {
        var drawList = ImGui.GetWindowDrawList();
        var usedFont = font ?? ImGui.GetFont();

        ImGui.PushFont(usedFont);
        Vector2 rawSize;
        float usedEffectiveSize;
        try
        {
            usedEffectiveSize = ImGui.GetFontSize();
            rawSize = ImGui.CalcTextSize(seString.TextValue);  
        }
        finally
        {
            ImGui.PopFont();
        }

        var desiredSize = targetFontSize ?? usedEffectiveSize;
        var scale = usedEffectiveSize > 0 ? (desiredSize / usedEffectiveSize) : 1f;

        var textSize = rawSize * scale;

        var style = ImGui.GetStyle();
        var frameHeight = desiredSize + style.FramePadding.Y * 2f;
        var hitboxHeight = MathF.Max(frameHeight, textSize.Y);
        var verticalOffset = MathF.Max((hitboxHeight - textSize.Y) * 0.5f, 0f);

        var drawPos = new Vector2(position.X, position.Y + verticalOffset);

        var drawParams = new SeStringDrawParams
        {
            TargetDrawList = drawList,
            ScreenOffset = drawPos,
            Font = usedFont,
            FontSize = desiredSize,
            Color = 0xFFFFFFFF,
            WrapWidth = float.MaxValue,
        };

        ImGui.SetCursorScreenPos(drawPos);
        ImGuiHelpers.SeStringWrapped(seString.Encode(), drawParams);

        ImGui.SetCursorScreenPos(position);
        ImGui.PushID(id ?? Interlocked.Increment(ref _seStringHitboxCounter).ToString());

        try
        {
            ImGui.InvisibleButton("##hitbox", new Vector2(textSize.X, hitboxHeight));
        }
        finally
        {
            ImGui.PopID();
        }

        return new Vector2(textSize.X, hitboxHeight);
    }

    public static Vector2 RenderIconWithHitbox(int iconId, Vector2 position, ImFontPtr? font = null, string? id = null)
    {
        var drawList = ImGui.GetWindowDrawList();
        var usedFont = font ?? UiBuilder.MonoFont;
        var iconMacro = $"<icon({iconId})>";

        var measureParams = new SeStringDrawParams
        {
            Font = usedFont,
            FontSize = usedFont.FontSize,
            Color = 0xFFFFFFFF,
            WrapWidth = float.MaxValue
        };

        var measureResult = ImGuiHelpers.CompileSeStringWrapped(iconMacro, measureParams);
        var iconSize = measureResult.Size;
        if (iconSize.Y <= 0f)
        {
            iconSize.Y = usedFont.FontSize > 0f ? usedFont.FontSize : ImGui.GetFontSize();
        }

        var style = ImGui.GetStyle();
        var fontHeight = usedFont.FontSize > 0f ? usedFont.FontSize : ImGui.GetFontSize();
        var frameHeight = fontHeight + style.FramePadding.Y * 2f;
        var hitboxHeight = MathF.Max(frameHeight, iconSize.Y);
        var verticalOffset = MathF.Max((hitboxHeight - iconSize.Y) * 0.5f, 0f);

        var drawPos = new Vector2(position.X, position.Y + verticalOffset);
        var drawParams = new SeStringDrawParams
        {
            Font = usedFont,
            FontSize = usedFont.FontSize,
            Color = 0xFFFFFFFF,
            WrapWidth = float.MaxValue,
            TargetDrawList = drawList,
            ScreenOffset = drawPos
        };
        ImGuiHelpers.CompileSeStringWrapped(iconMacro, drawParams);

        ImGui.SetCursorScreenPos(position);
        if (id is not null)
        {
            ImGui.PushID(id);
        }
        else
        {
            ImGui.PushID(Interlocked.Increment(ref _iconHitboxCounter));
        }

        try
        {
            ImGui.InvisibleButton("##iconHitbox", new Vector2(iconSize.X, hitboxHeight));
        }
        finally
        {
            ImGui.PopID();
        }

        return new Vector2(iconSize.X, hitboxHeight);
    }

    #region Internal Payloads

    public readonly record struct RichTextEntry(string Text, Vector4? Color = null, bool Bold = false);

    private abstract class AbstractColorPayload : Payload
    {
        protected byte Red { get; init; }
        protected byte Green { get; init; }
        protected byte Blue { get; init; }

        protected override byte[] EncodeImpl()
        {
            return new byte[] { 0x02, ChunkType, 0x05, 0xF6, Red, Green, Blue, 0x03 };
        }

        protected override void DecodeImpl(BinaryReader reader, long endOfStream) { }

        public override PayloadType Type => PayloadType.Unknown;
        protected abstract byte ChunkType { get; }
    }

    private abstract class AbstractColorEndPayload : Payload
    {
        protected override byte[] EncodeImpl()
        {
            return new byte[] { 0x02, ChunkType, 0x02, 0xEC, 0x03 };
        }

        protected override void DecodeImpl(BinaryReader reader, long endOfStream) { }

        public override PayloadType Type => PayloadType.Unknown;
        protected abstract byte ChunkType { get; }
    }

    private sealed class ColorPayload : AbstractColorPayload
    {
        protected override byte ChunkType => 0x13;

        public ColorPayload(Vector3 color)
        {
            Red = Math.Max((byte)1, (byte)(color.X * 255f));
            Green = Math.Max((byte)1, (byte)(color.Y * 255f));
            Blue = Math.Max((byte)1, (byte)(color.Z * 255f));
        }

        public ColorPayload(Vector4 color) : this(new Vector3(color.X, color.Y, color.Z)) { }
    }

    private sealed class ColorEndPayload : AbstractColorEndPayload
    {
        protected override byte ChunkType => 0x13;
    }

    private sealed class GlowPayload : AbstractColorPayload
    {
        protected override byte ChunkType => 0x14;

        public GlowPayload(Vector3 color)
        {
            Red = Math.Max((byte)1, (byte)(color.X * 255f));
            Green = Math.Max((byte)1, (byte)(color.Y * 255f));
            Blue = Math.Max((byte)1, (byte)(color.Z * 255f));
        }

        public GlowPayload(Vector4 color) : this(new Vector3(color.X, color.Y, color.Z)) { }
    }

    private sealed class GlowEndPayload : AbstractColorEndPayload
    {
        protected override byte ChunkType => 0x14;
    }

    #endregion
}
