using Dalamud;
using Dalamud.Bindings.ImGui;
using Dalamud.Interface;
using Dalamud.Interface.FontIdentifier;
using Dalamud.Interface.GameFonts;
using Dalamud.Interface.ManagedFontAtlas;
using Dalamud.Interface.Utility;
using Dalamud.Plugin;

namespace HellionChat;

// Two LogProxy sites live in static methods (TryGetBundledFontBytes,
// AddFontWithFallback); a ctor-injected ILogger would not be reachable
// from those scopes, so the class stays on Plugin.LogProxy.
//
// Hybrid handle model: Axis and AxisItalic mirror the game's current
// font state and are init-only. FontAwesome reuses Dalamud's UiBuilder
// fixed-width icon handle and is likewise init-only. RegularFont and
// ItalicFont depend on user-toggleable settings and get replaced live
// via RebuildDelegateFonts when those settings change; they stay as
// mutable nullable fields.
//
// The four atlas-owned handles register inside a single
// SuppressAutoRebuild block so the font atlas only rebuilds once for the
// whole plugin start instead of once per handle. FontAwesome lives
// outside that accounting because the UiBuilder already owns it.
public sealed class FontManager : IDisposable
{
    private readonly IDalamudPluginInterface _pluginInterface;

    internal IFontHandle Axis { get; init; }
    internal IFontHandle AxisItalic { get; init; }
    internal IFontHandle FontAwesome { get; init; }

    // Mutable because the live font settings replace these via
    // RebuildDelegateFonts. Reference replacement is atomic for reference
    // types, so push sites that read the field once per frame see at most
    // one stale handle.
    internal IFontHandle? RegularFont;
    internal IFontHandle? ItalicFont;

    private ushort[] Ranges = [];
    private ushort[] JpRange = [];

    public static readonly HashSet<float> AxisFontSizeList =
    [
        9.6f,
        10f,
        12f,
        14f,
        16f,
        18f,
        18.4f,
        20f,
        23f,
        34f,
        36f,
        40f,
        45f,
        46f,
        68f,
        90f,
    ];

    // Bundled UI font bytes (Inter Light, OFL-1.1); lazily loaded from manifest resources
    private static byte[]? BundledFontBytes;

    public FontManager(IDalamudPluginInterface pluginInterface)
    {
        _pluginInterface = pluginInterface;
        SetUpRanges();

        var atlas = _pluginInterface.UiBuilder.FontAtlas;

        using (atlas.SuppressAutoRebuild())
        {
            Axis = atlas.NewGameFontHandle(
                new GameFontStyle(GameFontFamily.Axis, SizeInPx(Plugin.Config.FontSizeV2))
            );

            AxisItalic = atlas.NewGameFontHandle(
                new GameFontStyle(GameFontFamily.Axis, SizeInPx(Plugin.Config.FontSizeV2))
                {
                    SkewStrength = SizeInPx(Plugin.Config.FontSizeV2) / 6,
                }
            );

            FontAwesome = _pluginInterface.UiBuilder.IconFontFixedWidthHandle;

            RegularFont = BuildRegularFontHandle(atlas);

            if (Plugin.Config.ItalicEnabled)
                ItalicFont = BuildItalicFontHandle(atlas);
        }
    }

    // Called from the settings save path when one of the font-related
    // settings changed. Game fonts and FontAwesome stay untouched because
    // none of those settings affect them.
    //
    // Thread model: the settings save path runs on the ImGui draw thread,
    // same as every push site. The rebuild finishes synchronously before
    // the next push reads the field in the same frame, so there is no
    // cross-thread race on the handle reference.
    public void RebuildDelegateFonts()
    {
        SetUpRanges();

        var atlas = _pluginInterface.UiBuilder.FontAtlas;

        RegularFont?.Dispose();
        RegularFont = BuildRegularFontHandle(atlas);

        ItalicFont?.Dispose();
        ItalicFont = Plugin.Config.ItalicEnabled ? BuildItalicFontHandle(atlas) : null;
    }

    // Instance method so Ranges / JpRange are reachable without parameter
    // plumbing; PascalCase field names follow the existing class style.
    private IFontHandle BuildRegularFontHandle(IFontAtlas atlas) =>
        atlas.NewDelegateFontHandle(e =>
            e.OnPreBuild(tk =>
            {
                // UseHellionFont swaps the source font but keeps the size
                // selector tied to FontSizeV2 (the bundled font ships as
                // a single weight).
                var basePt = Plugin.Config.UseHellionFont
                    ? Plugin.Config.FontSizeV2
                    : Plugin.Config.GlobalFontV2.SizePt;
                var config = new SafeFontConfig { SizePt = basePt, GlyphRanges = Ranges };
                // Missing embedded resource falls back to the configured
                // system font instead of taking the whole UiBuilder down.
                var bundledBytes = Plugin.Config.UseHellionFont ? TryGetBundledFontBytes() : null;
                config.MergeFont = bundledBytes is not null
                    ? tk.AddFontFromMemory(bundledBytes, config, "Inter-Light")
                    : AddFontWithFallback(tk, Plugin.Config.GlobalFontV2.FontId, config, "global");

                config.SizePt = Plugin.Config.JapaneseFontV2.SizePt;
                config.GlyphRanges = JpRange;
                AddFontWithFallback(tk, Plugin.Config.JapaneseFontV2.FontId, config, "japanese");

                // v1.5.3: NotoSansCjk fallback covers Hangul, Simplified-Chinese
                // -specific Han (e.g. 简) and other CJK glyphs that the primary
                // (Inter Light / global font) and the FFXIV Japanese font do not
                // ship. Merged last so earlier fonts win for shared codepoints.
                config.SizePt = basePt;
                config.GlyphRanges = Ranges;
                AddFontWithFallback(
                    tk,
                    new DalamudAssetFontAndFamilyId(DalamudAsset.NotoSansCjkRegular),
                    config,
                    "noto-cjk-fallback"
                );

                config.SizePt = Plugin.Config.SymbolsFontSizeV2;
                tk.AddGameSymbol(config);

                tk.Font = config.MergeFont;
            })
        );

    private IFontHandle BuildItalicFontHandle(IFontAtlas atlas) =>
        atlas.NewDelegateFontHandle(e =>
            e.OnPreBuild(tk =>
            {
                var config = new SafeFontConfig
                {
                    SizePt = Plugin.Config.ItalicFontV2.SizePt,
                    GlyphRanges = Ranges,
                };
                config.MergeFont = AddFontWithFallback(
                    tk,
                    Plugin.Config.ItalicFontV2.FontId,
                    config,
                    "italic"
                );

                config.SizePt = Plugin.Config.JapaneseFontV2.SizePt;
                config.GlyphRanges = JpRange;
                AddFontWithFallback(tk, Plugin.Config.JapaneseFontV2.FontId, config, "japanese");

                // v1.5.3: NotoSansCjk fallback (see BuildRegularFontHandle).
                config.SizePt = Plugin.Config.ItalicFontV2.SizePt;
                config.GlyphRanges = Ranges;
                AddFontWithFallback(
                    tk,
                    new DalamudAssetFontAndFamilyId(DalamudAsset.NotoSansCjkRegular),
                    config,
                    "noto-cjk-fallback"
                );

                config.SizePt = Plugin.Config.SymbolsFontSizeV2;
                tk.AddGameSymbol(config);

                tk.Font = config.MergeFont;
            })
        );

    public void Dispose()
    {
        Axis.Dispose();
        AxisItalic.Dispose();
        // FontAwesome is shared with the UiBuilder; the host owns its
        // lifetime, so the plugin must not dispose it.
        RegularFont?.Dispose();
        ItalicFont?.Dispose();
    }

    // Returns null when the embedded font resource is missing. Should not
    // happen on a signed release build, but a broken csproj or hand-rolled
    // dev build can land here. Caller falls back to the system font path
    // so the plugin still loads instead of crashing the whole UiBuilder.
    private static byte[]? TryGetBundledFontBytes()
    {
        if (BundledFontBytes is not null)
            return BundledFontBytes;

        using var stream = typeof(FontManager).Assembly.GetManifestResourceStream(
            "Inter-Light.ttf"
        );
        if (stream is null)
        {
            Plugin.LogProxy.Warning(
                "Bundled Inter Light font resource missing, falling back to system default font."
            );
            return null;
        }

        using var ms = new MemoryStream();
        stream.CopyTo(ms);
        BundledFontBytes = ms.ToArray();
        return BundledFontBytes;
    }

    private unsafe void SetUpRanges()
    {
        ushort[] BuildRange(IReadOnlyList<ushort>? chars, params nint[] ranges)
        {
            var builder = new ImFontGlyphRangesBuilderPtr(ImGuiNative.ImFontGlyphRangesBuilder());
            foreach (var range in ranges)
                builder.AddRanges((ushort*)range);

            if (chars != null)
            {
                for (var i = 0; i < chars.Count; i += 2)
                {
                    if (chars[i] == 0)
                        break;

                    for (var j = (uint)chars[i]; j <= chars[i + 1]; j++)
                        builder.AddChar((ushort)j);
                }
            }

            // Ingame supported ranges
            var reader = new FdtReader(Plugin.DataManager.GetFile("common/font/axis_12.fdt")!.Data);
            foreach (var c in reader.Glyphs)
                builder.AddChar(c.Char);

            // French
            // Romanian
            builder.AddText("Œœ");
            builder.AddText("ĂăÂâÎîȘșȚț");

            // v1.5.3: language-dropdown endonyms. The dropdown renders
            // with the currently active font range; without these glyphs
            // a user on an English UI cannot read non-Latin language names
            // before switching. Auto-activation in Settings.Apply then
            // pulls in the full ExtraGlyphRange for the chosen locale.
            builder.AddText(
                "Català Čeština Dansk Deutsch Ελληνικά English Español Suomi"
                    + " Français Magyar Italiano 日本語 한국어 Norsk bokmål Nederlands"
                    + " Polski Português Brasil (Portugal) Română Русский Svenska"
                    + " Türkçe Українська 简体中文 繁體中文"
            );

            // "Enclosed Alphanumerics" (partial) https://www.compart.com/en/unicode/block/U+2460
            for (var i = 0x2460; i <= 0x24B5; i++)
                builder.AddChar((char)i);

            builder.AddChar('⓪');
            return builder.BuildRangesToArray();
        }

        var ranges = new List<nint> { (nint)ImGui.GetIO().Fonts.GetGlyphRangesDefault() };
        var customChars = new List<ushort>();
        foreach (var extraRange in Enum.GetValues<ExtraGlyphRanges>())
        {
            if (!Plugin.Config.ExtraGlyphRanges.HasFlag(extraRange))
                continue;

            // LatinExtended and Greek use AddChar pairs because they have no
            // built-in ImGui range helper; everything else points to a native
            // ImGui glyph-range table.
            switch (extraRange)
            {
                case ExtraGlyphRanges.LatinExtended:
                    customChars.AddRange(ExtraGlyphRangesExt.LatinExtendedPairs);
                    break;
                case ExtraGlyphRanges.Greek:
                    customChars.AddRange(ExtraGlyphRangesExt.GreekPairs);
                    break;
                default:
                    var ptr = extraRange.Range();
                    if (ptr != 0)
                        ranges.Add(ptr);
                    break;
            }
        }

        Ranges = BuildRange(customChars.Count > 0 ? customChars : null, ranges.ToArray());
        JpRange = BuildRange(GlyphRangesJapanese.GlyphRanges);
    }

    // Add font with fallback to NotoSansCjkRegular if unavailable
    private static ImFontPtr AddFontWithFallback(
        IFontAtlasBuildToolkitPreBuild tk,
        IFontId fontId,
        SafeFontConfig config,
        string slot
    )
    {
        try
        {
            return fontId.AddToBuildToolkit(tk, config);
        }
        catch (Exception e)
            when (e
                    is FileNotFoundException
                        or DirectoryNotFoundException
                        or IOException
                        or InvalidOperationException
                        or ArgumentException
            )
        {
            // Atlas-toolkit throws span IO and validation failures; routing
            // the wider set through the fallback keeps a corrupt font config
            // from taking down the whole atlas build.
            Plugin.LogProxy.Warning(
                e,
                $"Configured {slot} font failed to load ({e.GetType().Name}), "
                    + "falling back to NotoSansCjkRegular"
            );
            var fallback = new DalamudAssetFontAndFamilyId(DalamudAsset.NotoSansCjkRegular);
            return fallback.AddToBuildToolkit(tk, config);
        }
    }

    public static float SizeInPt(float px) => (float)(px * 3.0 / 4.0);

    public static float SizeInPx(float pt) => (float)(pt * 4.0 / 3.0);

    public static float GetFontSize() =>
        Plugin.Config.FontsEnabled
            ? Plugin.Config.GlobalFontV2.SizePx
            : SizeInPx(Plugin.Config.FontSizeV2);
}
