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

namespace HellionChat;

public class FontManager
{
    internal IFontHandle Axis = null!;
    internal IFontHandle AxisItalic = null!;

    internal IFontHandle RegularFont = null!;
    internal IFontHandle? ItalicFont;

    internal IFontHandle FontAwesome = null!;

    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,
    ];

    /// <summary>
    /// Backing bytes for the bundled Hellion font (Exo 2, OFL-1.1). Lazily
    /// extracted from the assembly's manifest resources on first use; the
    /// load happens inside the font atlas build callback so we keep the
    /// allocation off the plugin constructor's hot path.
    /// </summary>
    private static byte[]? HellionFontBytes;

    private static byte[] GetHellionFontBytes()
    {
        if (HellionFontBytes is not null)
            return HellionFontBytes;

        using var stream = typeof(FontManager).Assembly.GetManifestResourceStream("HellionFont.ttf")
            ?? throw new FileNotFoundException("Hellion font resource not embedded in the assembly");
        using var ms = new MemoryStream();
        stream.CopyTo(ms);
        HellionFontBytes = ms.ToArray();
        return HellionFontBytes;
    }

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

            // chars
            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);

            // various symbols
            // French
            // Romanian
            // builder.AddText("←→↑↓《》■※☀★★☆♥♡ヅツッシ☀☁☂℃℉°♀♂♠♣♦♣♧®©™€$£♯♭♪✓√◎◆◇♦■□〇●△▽▼▲‹›≤≥<«“”─＼～");
            builder.AddText("Œœ");
            builder.AddText("ĂăÂâÎîȘșȚț");

            // "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() };
        foreach (var extraRange in Enum.GetValues<ExtraGlyphRanges>())
            if (Plugin.Config.ExtraGlyphRanges.HasFlag(extraRange))
                ranges.Add(extraRange.Range());

        Ranges = BuildRange(null, ranges.ToArray());
        JpRange = BuildRange(GlyphRangesJapanese.GlyphRanges);
    }

    public void BuildFonts()
    {
        SetUpRanges();

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

        FontAwesome = Plugin.Interface.UiBuilder.FontAtlas.NewDelegateFontHandle(e =>
        {
            e.OnPreBuild(tk => tk.AddFontAwesomeIconFont(new SafeFontConfig { SizePx = GetFontSize() }));
            e.OnPostBuild(tk => tk.FitRatio(tk.Font));
        });

        RegularFont = Plugin.Interface.UiBuilder.FontAtlas.NewDelegateFontHandle(
            e => e.OnPreBuild(
                tk =>
                {
                    // v1.2.0 — Bei aktiver Hellion-Schrift (Exo 2 ist Variable-Font)
                    // wird die User-Schriftgröße aus FontSizeV2 als SizePt angewendet.
                    // Der Bestand-Pfad nutzt weiter GlobalFontV2.SizePt aus dem
                    // Custom-Font-Stack. Ohne diese Verzweigung war FontSizeV2 bei
                    // UseHellionFont=true wirkungslos, was 4K-User mit größerer
                    // Skalierung blockierte (Settings → Erscheinungsbild → Schriftarten).
                    var basePt = Plugin.Config.UseHellionFont
                        ? Plugin.Config.FontSizeV2
                        : Plugin.Config.GlobalFontV2.SizePt;
                    var config = new SafeFontConfig {SizePt = basePt, GlyphRanges = Ranges};
                    config.MergeFont = Plugin.Config.UseHellionFont
                        ? tk.AddFontFromMemory(GetHellionFontBytes(), config, "Hellion-Exo2")
                        : 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");

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

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

        if (Plugin.Config.ItalicEnabled)
        {
            ItalicFont = Plugin.Interface.UiBuilder.FontAtlas.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");

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

                        tk.Font = config.MergeFont;
                    }
                ));
        }
        else
        {
            ItalicFont = null;
        }
    }

    /// <summary>
    /// Try to add a user-configured font to the build toolkit, falling back to
    /// the bundled NotoSansCjkRegular asset if the configured font isn't
    /// available on the system. Without this guard a stale SystemFontId
    /// pointing at a font the user uninstalled or that never existed on
    /// Linux (e.g. "Crimson Text") tears down the entire font atlas build.
    /// </summary>
    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)
        {
            Plugin.Log.Warning(e, $"Configured {slot} font unavailable, 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);
}
