using Dalamud.Plugin.Services;
using Microsoft.Extensions.Logging;
using Penumbra.GameData.Files;

namespace LightlessSync.Services.TextureCompression;

// ima lie, this isn't garbage

public sealed class TextureMetadataHelper
{
    private readonly ILogger<TextureMetadataHelper> _logger;
    private readonly IDataManager _dataManager;

    private static readonly Dictionary<TextureCompressionTarget, (string Title, string Description)> RecommendationCatalog = new()
    {
        [TextureCompressionTarget.BC1] = (
            "BC1 (Simple Compression for Opaque RGB)",
            "This offers a 8:1 compression ratio and is quick with acceptable quality, but only supports RGB, without Alpha.\n\nCan be used for diffuse maps and equipment textures to save extra space."),
        [TextureCompressionTarget.BC3] = (
            "BC3 (Simple Compression for RGBA)",
            "This offers a 4:1 compression ratio and is quick with acceptable quality, and fully supports RGBA.\n\nGeneric format that can be used for most textures."),
        [TextureCompressionTarget.BC4] = (
            "BC4 (Simple Compression for Opaque Grayscale)",
            "This offers a 8:1 compression ratio and has almost indistinguishable quality, but only supports Grayscale, without Alpha.\n\nCan be used for face paints and legacy marks."),
        [TextureCompressionTarget.BC5] = (
            "BC5 (Simple Compression for Opaque RG)",
            "This offers a 4:1 compression ratio and has almost indistinguishable quality, but only supports RG, without B or Alpha.\n\nRecommended for index maps, unrecommended for normal maps."),
        [TextureCompressionTarget.BC7] = (
            "BC7 (Complex Compression for RGBA)",
            "This offers a 4:1 compression ratio and has almost indistinguishable quality, but may take a while.\n\nGeneric format that can be used for most textures.")
    };

    private static readonly (TextureUsageCategory Category, string Token)[] CategoryTokens =
    {
        (TextureUsageCategory.UI, "/ui/"),
        (TextureUsageCategory.UI, "/uld/"),
        (TextureUsageCategory.UI, "/icon/"),

        (TextureUsageCategory.VisualEffect, "/vfx/"),

        (TextureUsageCategory.Customization, "/chara/human/"),
        (TextureUsageCategory.Customization, "/chara/common/"),
        (TextureUsageCategory.Customization, "/chara/bibo"),

        (TextureUsageCategory.Weapon, "/chara/weapon/"),

        (TextureUsageCategory.Accessory, "/chara/accessory/"),

        (TextureUsageCategory.Gear, "/chara/equipment/"),

        (TextureUsageCategory.Monster, "/chara/monster/"),
        (TextureUsageCategory.Monster, "/chara/demihuman/"),

        (TextureUsageCategory.MountOrMinion, "/chara/mount/"),
        (TextureUsageCategory.MountOrMinion, "/chara/battlepet/"),

        (TextureUsageCategory.Companion, "/chara/companion/"),

        (TextureUsageCategory.Housing, "/hou/"),
        (TextureUsageCategory.Housing, "/housing/"),
        (TextureUsageCategory.Housing, "/bg/"),
        (TextureUsageCategory.Housing, "/bgcommon/")
    };

    private static readonly (TextureUsageCategory Category, string SlotToken, string SlotName)[] SlotTokens =
    {
        (TextureUsageCategory.Gear, "_met", "Head"),

        (TextureUsageCategory.Gear, "_top", "Body"),

        (TextureUsageCategory.Gear, "_glv", "Hands"),

        (TextureUsageCategory.Gear, "_dwn", "Legs"),

        (TextureUsageCategory.Gear, "_sho", "Feet"),

        (TextureUsageCategory.Accessory, "_ear", "Ears"),

        (TextureUsageCategory.Accessory, "_nek", "Neck"),

        (TextureUsageCategory.Accessory, "_wrs", "Wrists"),

        (TextureUsageCategory.Accessory, "_rir", "Ring"),

        (TextureUsageCategory.Weapon, "_w", "Weapon"), // sussy
        (TextureUsageCategory.Weapon, "weapon", "Weapon"),
    };

    private static readonly (TextureMapKind Kind, string Token)[] MapTokens =
    {
        (TextureMapKind.Normal, "_n."),
        (TextureMapKind.Normal, "_n_"),
        (TextureMapKind.Normal, "_normal"),
        (TextureMapKind.Normal, "normal_"),
        (TextureMapKind.Normal, "_norm"),
        (TextureMapKind.Normal, "norm_"),

        (TextureMapKind.Mask, "_m."),
        (TextureMapKind.Mask, "_m_"),
        (TextureMapKind.Mask, "_mask"),
        (TextureMapKind.Mask, "mask_"),
        (TextureMapKind.Mask, "_msk"),

        (TextureMapKind.Specular, "_s."),
        (TextureMapKind.Specular, "_s_"),
        (TextureMapKind.Specular, "_spec"),
        (TextureMapKind.Specular, "_specular"),
        (TextureMapKind.Specular, "specular_"),

        (TextureMapKind.Index, "_id."),
        (TextureMapKind.Index, "_id_"),
        (TextureMapKind.Index, "_idx"),
        (TextureMapKind.Index, "_index"),
        (TextureMapKind.Index, "index_"),
        (TextureMapKind.Index, "_multi"),

        (TextureMapKind.Diffuse, "_d."),
        (TextureMapKind.Diffuse, "_d_"),
        (TextureMapKind.Diffuse, "_diff"),
        (TextureMapKind.Diffuse, "_b."),
        (TextureMapKind.Diffuse, "_b_"),
        (TextureMapKind.Diffuse, "_base"),
        (TextureMapKind.Diffuse, "base_")
    };

    private const string TextureSegment = "/texture/";
    private const string MaterialSegment = "/material/";

    private const uint NormalSamplerId = ShpkFile.NormalSamplerId;
    private const uint IndexSamplerId = ShpkFile.IndexSamplerId;
    private const uint SpecularSamplerId = ShpkFile.SpecularSamplerId;
    private const uint DiffuseSamplerId = ShpkFile.DiffuseSamplerId;
    private const uint MaskSamplerId = ShpkFile.MaskSamplerId;

    public TextureMetadataHelper(ILogger<TextureMetadataHelper> logger, IDataManager dataManager)
    {
        _logger = logger;
        _dataManager = dataManager;
    }

    public static bool TryGetRecommendationInfo(TextureCompressionTarget target, out (string Title, string Description) info)
        => RecommendationCatalog.TryGetValue(target, out info);

    public static TextureUsageCategory DetermineCategory(string? gamePath)
    {
        var normalized = Normalize(gamePath);
        if (string.IsNullOrEmpty(normalized))
            return TextureUsageCategory.Unknown;

        var fileName = Path.GetFileName(normalized);
        if (!string.IsNullOrEmpty(fileName))
        {
            if (fileName.StartsWith("bibo", StringComparison.OrdinalIgnoreCase)
                || fileName.Contains("gen3", StringComparison.OrdinalIgnoreCase)
                || fileName.Contains("tfgen3", StringComparison.OrdinalIgnoreCase))
            {
                return TextureUsageCategory.Customization;
            }
        }

        if (normalized.Contains("bibo", StringComparison.OrdinalIgnoreCase)
            || normalized.Contains("skin", StringComparison.OrdinalIgnoreCase)
            || normalized.Contains("gen3", StringComparison.OrdinalIgnoreCase)
            || normalized.Contains("tfgen3", StringComparison.OrdinalIgnoreCase)
            || normalized.Contains("body", StringComparison.OrdinalIgnoreCase))
        {
            return TextureUsageCategory.Customization;
        }

        foreach (var (category, token) in CategoryTokens)
        {
            if (normalized.Contains(token, StringComparison.OrdinalIgnoreCase))
                return category;
        }

        var segments = normalized.Split('/', StringSplitOptions.RemoveEmptyEntries);
        if (segments.Length >= 2 && string.Equals(segments[0], "chara", StringComparison.OrdinalIgnoreCase))
        {
            return segments[1] switch
            {
                "equipment" => TextureUsageCategory.Gear,
                "accessory" => TextureUsageCategory.Accessory,
                "weapon" => TextureUsageCategory.Weapon,
                "human" or "common" => TextureUsageCategory.Customization,
                "monster" or "demihuman" => TextureUsageCategory.Monster,
                "mount" or "battlepet" => TextureUsageCategory.MountOrMinion,
                "companion" => TextureUsageCategory.Companion,
                _ => TextureUsageCategory.Unknown
            };
        }

        if (normalized.StartsWith("chara/", StringComparison.OrdinalIgnoreCase)
            && (normalized.Contains("bibo", StringComparison.OrdinalIgnoreCase)
                || normalized.Contains("skin", StringComparison.OrdinalIgnoreCase)
                || normalized.Contains("body", StringComparison.OrdinalIgnoreCase)))
            return TextureUsageCategory.Customization;

        return TextureUsageCategory.Unknown;
    }

    public static string DetermineSlot(TextureUsageCategory category, string? gamePath)
    {
        if (category == TextureUsageCategory.Customization)
            return GuessCustomizationSlot(gamePath);

        var normalized = Normalize(gamePath);
        var fileName = Path.GetFileNameWithoutExtension(normalized);
        var searchSource = $"{normalized} {fileName}".ToLowerInvariant();

        foreach (var (candidateCategory, token, slot) in SlotTokens)
        {
            if (candidateCategory == category && searchSource.Contains(token, StringComparison.Ordinal))
                return slot;
        }

        return category switch
        {
            TextureUsageCategory.Gear => "Gear",
            TextureUsageCategory.Accessory => "Accessory",
            TextureUsageCategory.Weapon => "Weapon",
            TextureUsageCategory.Monster => "Monster",
            TextureUsageCategory.MountOrMinion => "Mount / Minion",
            TextureUsageCategory.Companion => "Companion",
            TextureUsageCategory.VisualEffect => "VFX",
            TextureUsageCategory.Housing => "Housing",
            TextureUsageCategory.UI => "UI",
            _ => "General"
        };
    }

    public TextureMapKind DetermineMapKind(string path)
        => DetermineMapKind(path, null);

    public TextureMapKind DetermineMapKind(string? gamePath, string? localTexturePath)
    {
        if (TryDetermineFromMaterials(gamePath, localTexturePath, out var kind))
            return kind;

        return GuessMapFromFileName(gamePath ?? localTexturePath ?? string.Empty);
    }

    private bool TryDetermineFromMaterials(string? gamePath, string? localTexturePath, out TextureMapKind kind)
    {
        kind = TextureMapKind.Unknown;

        var candidates = new List<MaterialCandidate>();
        AddGameMaterialCandidates(gamePath, candidates);
        AddLocalMaterialCandidates(localTexturePath, candidates);

        if (candidates.Count == 0)
            return false;

        var normalizedGamePath = Normalize(gamePath);
        var normalizedFileName = Path.GetFileName(normalizedGamePath);

        foreach (var candidate in candidates)
        {
            if (!TryLoadMaterial(candidate, out var material))
                continue;

            if (TryInferKindFromMaterial(material, normalizedGamePath, normalizedFileName, out kind))
                return true;
        }

        return false;
    }

    private static void AddGameMaterialCandidates(string? gamePath, IList<MaterialCandidate> candidates)
    {
        var normalized = Normalize(gamePath);
        if (string.IsNullOrEmpty(normalized))
            return;

        var textureIndex = normalized.IndexOf(TextureSegment, StringComparison.Ordinal);
        if (textureIndex < 0)
            return;

        var prefix = normalized[..textureIndex];
        var suffix = normalized[(textureIndex + TextureSegment.Length)..];
        var baseName = Path.GetFileNameWithoutExtension(suffix);
        if (string.IsNullOrEmpty(baseName))
            return;

        var directory = $"{prefix}{MaterialSegment}{Path.GetDirectoryName(suffix)?.Replace('\\', '/') ?? string.Empty}".TrimEnd('/');
        candidates.Add(MaterialCandidate.Game($"{directory}/mt_{baseName}.mtrl"));

        if (baseName.StartsWith('v') && baseName.IndexOf('_') is > 0 and var idx)
        {
            var trimmed = baseName[(idx + 1)..];
            candidates.Add(MaterialCandidate.Game($"{directory}/mt_{trimmed}.mtrl"));
        }
    }

    private static void AddLocalMaterialCandidates(string? localTexturePath, IList<MaterialCandidate> candidates)
    {
        if (string.IsNullOrEmpty(localTexturePath))
            return;

        var normalized = localTexturePath.Replace('\\', '/');
        var textureIndex = normalized.IndexOf(TextureSegment, StringComparison.OrdinalIgnoreCase);
        if (textureIndex >= 0)
        {
            var prefix = normalized[..textureIndex];
            var suffix = normalized[(textureIndex + TextureSegment.Length)..];
            var folder = Path.GetDirectoryName(suffix)?.Replace('\\', '/') ?? string.Empty;
            var baseName = Path.GetFileNameWithoutExtension(suffix);
            if (!string.IsNullOrEmpty(baseName))
            {
                var materialDir = $"{prefix}{MaterialSegment}{folder}".TrimEnd('/');
                candidates.Add(MaterialCandidate.Local(Path.Combine(materialDir.Replace('/', Path.DirectorySeparatorChar), $"mt_{baseName}.mtrl")));

                if (baseName.StartsWith('v') && baseName.IndexOf('_') is > 0 and var idx)
                {
                    var trimmed = baseName[(idx + 1)..];
                    candidates.Add(MaterialCandidate.Local(Path.Combine(materialDir.Replace('/', Path.DirectorySeparatorChar), $"mt_{trimmed}.mtrl")));
                }
            }
        }

        var textureDirectory = Path.GetDirectoryName(localTexturePath);
        if (!string.IsNullOrEmpty(textureDirectory) && Directory.Exists(textureDirectory))
        {
            foreach (var candidate in Directory.EnumerateFiles(textureDirectory, "*.mtrl", SearchOption.TopDirectoryOnly))
                candidates.Add(MaterialCandidate.Local(candidate));
        }
    }

    private bool TryLoadMaterial(MaterialCandidate candidate, out MtrlFile material)
    {
        material = null!;

        try
        {
            switch (candidate.Source)
            {
                case MaterialSource.Game:
                    var gameFile = _dataManager.GetFile(candidate.Path);
                    if (gameFile?.Data.Length > 0)
                    {
                        material = new MtrlFile(gameFile.Data);
                        return material.Valid;
                    }
                    break;

                case MaterialSource.Local when File.Exists(candidate.Path):
                    material = new MtrlFile(File.ReadAllBytes(candidate.Path));
                    return material.Valid;
            }
        }
        catch (Exception ex)
        {
            _logger.LogDebug(ex, "Failed to load material {Path}", candidate.Path);
        }

        return false;
    }

    private static bool TryInferKindFromMaterial(MtrlFile material, string normalizedGamePath, string? fileName, out TextureMapKind kind)
    {
        kind = TextureMapKind.Unknown;
        var targetName = fileName ?? string.Empty;

        foreach (var sampler in material.ShaderPackage.Samplers)
        {
            if (!TryMapSamplerId(sampler.SamplerId, out var candidateKind))
                continue;

            if (sampler.TextureIndex < 0 || sampler.TextureIndex >= material.Textures.Length)
                continue;

            var texturePath = Normalize(material.Textures[sampler.TextureIndex].Path);

            if (!string.IsNullOrEmpty(normalizedGamePath) && string.Equals(texturePath, normalizedGamePath, StringComparison.OrdinalIgnoreCase))
            {
                kind = candidateKind;
                return true;
            }

            if (!string.IsNullOrEmpty(targetName) && string.Equals(Path.GetFileName(texturePath), targetName, StringComparison.OrdinalIgnoreCase))
            {
                kind = candidateKind;
                return true;
            }
        }

        return false;
    }

    private static TextureMapKind GuessMapFromFileName(string path)
    {
        var normalized = Normalize(path);
        var fileNameWithExtension = Path.GetFileName(normalized);
        var fileNameWithoutExtension = Path.GetFileNameWithoutExtension(normalized);
        if (string.IsNullOrEmpty(fileNameWithExtension) && string.IsNullOrEmpty(fileNameWithoutExtension))
            return TextureMapKind.Unknown;

        if (normalized.Contains("/eye/eyelids_shadow.tex", StringComparison.Ordinal))
            return TextureMapKind.Normal;

        if (normalized.Contains("/ui/map/", StringComparison.Ordinal) && !string.IsNullOrEmpty(fileNameWithoutExtension))
        {
            if (fileNameWithoutExtension.EndsWith("m_m", StringComparison.Ordinal)
                || fileNameWithoutExtension.EndsWith("m_s", StringComparison.Ordinal))
                return TextureMapKind.Mask;

            if (fileNameWithoutExtension.EndsWith("_m", StringComparison.Ordinal)
                || fileNameWithoutExtension.EndsWith("_s", StringComparison.Ordinal)
                || fileNameWithoutExtension.EndsWith("d", StringComparison.Ordinal))
                return TextureMapKind.Diffuse;
        }

        foreach (var (kind, token) in MapTokens)
        {
            if (!string.IsNullOrEmpty(fileNameWithExtension) &&
                fileNameWithExtension.Contains(token, StringComparison.OrdinalIgnoreCase))
                return kind;

            if (!string.IsNullOrEmpty(fileNameWithoutExtension) &&
                fileNameWithoutExtension.Contains(token, StringComparison.OrdinalIgnoreCase))
                return kind;
        }

        return TextureMapKind.Unknown;
    }

    private static readonly (string Token, TextureCompressionTarget Target)[] FormatTargetTokens =
    {
        ("BC1", TextureCompressionTarget.BC1),
        ("DXT1", TextureCompressionTarget.BC1),
        ("BC3", TextureCompressionTarget.BC3),
        ("DXT3", TextureCompressionTarget.BC3),
        ("DXT5", TextureCompressionTarget.BC3),
        ("BC4", TextureCompressionTarget.BC4),
        ("ATI1", TextureCompressionTarget.BC4),
        ("BC5", TextureCompressionTarget.BC5),
        ("ATI2", TextureCompressionTarget.BC5),
        ("3DC", TextureCompressionTarget.BC5),
        ("BC7", TextureCompressionTarget.BC7),
        ("BPTC", TextureCompressionTarget.BC7)
    }; // idk man

    public static bool TryMapFormatToTarget(string? format, out TextureCompressionTarget target)
    {
        var normalized = (format ?? string.Empty).ToUpperInvariant();
        foreach (var (token, mappedTarget) in FormatTargetTokens)
        {
            if (normalized.Contains(token, StringComparison.Ordinal))
            {
                target = mappedTarget;
                return true;
            }
        }

        target = TextureCompressionTarget.BC7;
        return false;
    }

    public static (TextureCompressionTarget Target, string Reason)? GetSuggestedTarget(
        string? format,
        TextureMapKind mapKind,
        string? texturePath = null)
    {
        TextureCompressionTarget? current = null;
        if (TryMapFormatToTarget(format, out var mapped))
            current = mapped;

        var prefersBc4 = IsFacePaintOrMarkTexture(texturePath);

        var suggestion = mapKind switch
        {
            TextureMapKind.Normal => TextureCompressionTarget.BC7,
            TextureMapKind.Mask => TextureCompressionTarget.BC7,
            TextureMapKind.Index => TextureCompressionTarget.BC5,
            TextureMapKind.Specular => TextureCompressionTarget.BC3,
            TextureMapKind.Diffuse => TextureCompressionTarget.BC7,
            _ => TextureCompressionTarget.BC7
        };

        if (prefersBc4)
        {
            suggestion = TextureCompressionTarget.BC4;
        }
        else if (mapKind == TextureMapKind.Diffuse && current is null && !HasAlphaHint(format))
            suggestion = TextureCompressionTarget.BC1;

        if (current == suggestion)
            return null;

        return (suggestion, RecommendationCatalog.TryGetValue(suggestion, out var info)
            ? info.Description
            : "Suggested to balance visual quality and file size.");
    }

    private static bool TryMapSamplerId(uint id, out TextureMapKind kind)
    {
        kind = id switch
        {
            NormalSamplerId => TextureMapKind.Normal,
            IndexSamplerId => TextureMapKind.Index,
            SpecularSamplerId => TextureMapKind.Specular,
            DiffuseSamplerId => TextureMapKind.Diffuse,
            MaskSamplerId => TextureMapKind.Mask,
            _ => TextureMapKind.Unknown
        };

        return kind != TextureMapKind.Unknown;
    }

    private static string GuessCustomizationSlot(string? gamePath)
    {
        var normalized = Normalize(gamePath);
        var fileName = Path.GetFileName(normalized);

        if (!string.IsNullOrEmpty(fileName))
        {
            if (fileName.StartsWith("bibo", StringComparison.OrdinalIgnoreCase)
                || fileName.Contains("gen3", StringComparison.OrdinalIgnoreCase)
                || fileName.Contains("tfgen3", StringComparison.OrdinalIgnoreCase)
                || fileName.Contains("skin", StringComparison.OrdinalIgnoreCase))
            {
                return "Skin";
            }
        }

        if (normalized.Contains("hair", StringComparison.OrdinalIgnoreCase))
            return "Hair";
        if (normalized.Contains("face", StringComparison.OrdinalIgnoreCase))
            return "Face";
        if (normalized.Contains("tail", StringComparison.OrdinalIgnoreCase))
            return "Tail";
        if (normalized.Contains("zear", StringComparison.OrdinalIgnoreCase))
            return "Ear";
        if (normalized.Contains("eye", StringComparison.OrdinalIgnoreCase))
            return "Eye";
        if (normalized.Contains("body", StringComparison.OrdinalIgnoreCase)
            || normalized.Contains("skin", StringComparison.OrdinalIgnoreCase)
            || normalized.Contains("bibo", StringComparison.OrdinalIgnoreCase))
            return "Skin";
        if (IsFacePaintPath(normalized))
            return "Face Paint";
        if (IsLegacyMarkPath(normalized))
            return "Legacy Mark";
        if (normalized.Contains("decal_equip", StringComparison.OrdinalIgnoreCase))
            return "Equipment Decal";

        return "Customization";
    }

    private static bool IsFacePaintOrMarkTexture(string? texturePath)
    {
        var normalized = Normalize(texturePath);
        return IsFacePaintPath(normalized) || IsLegacyMarkPath(normalized);
    }

    private static bool IsFacePaintPath(string? normalizedPath)
    {
        if (string.IsNullOrEmpty(normalizedPath))
            return false;

        return normalizedPath.Contains("decal_face", StringComparison.Ordinal)
            || normalizedPath.Contains("facepaint", StringComparison.Ordinal)
            || normalizedPath.Contains("_decal_", StringComparison.Ordinal);
    }

    private static bool IsLegacyMarkPath(string? normalizedPath)
    {
        if (string.IsNullOrEmpty(normalizedPath))
            return false;

        return normalizedPath.Contains("transparent", StringComparison.Ordinal)
            || normalizedPath.Contains("transparent.tex", StringComparison.Ordinal);
    }

    private static bool HasAlphaHint(string? format)
    {
        if (string.IsNullOrEmpty(format))
            return false;

        var normalized = format.ToUpperInvariant();
        return normalized.Contains("A8", StringComparison.Ordinal)
            || normalized.Contains("A1", StringComparison.Ordinal)
            || normalized.Contains("A4", StringComparison.Ordinal)
            || normalized.Contains("A16", StringComparison.Ordinal)
            || normalized.Contains("A32", StringComparison.Ordinal)
            || normalized.Contains("ARGB", StringComparison.Ordinal)
            || normalized.Contains("RGBA", StringComparison.Ordinal)
            || normalized.Contains("BGRA", StringComparison.Ordinal)
            || normalized.Contains("DXT3", StringComparison.Ordinal)
            || normalized.Contains("DXT5", StringComparison.Ordinal)
            || normalized.Contains("BC2", StringComparison.Ordinal)
            || normalized.Contains("BC3", StringComparison.Ordinal)
            || normalized.Contains("BC7", StringComparison.Ordinal);
    }

    private static string Normalize(string? path)
    {
        if (string.IsNullOrWhiteSpace(path))
            return string.Empty;

        return path.Replace('\\', '/').ToLowerInvariant();
    }

    private readonly record struct MaterialCandidate(string Path, MaterialSource Source)
    {
        public static MaterialCandidate Game(string path) => new(path, MaterialSource.Game);
        public static MaterialCandidate Local(string path) => new(path, MaterialSource.Local);
    }

    private enum MaterialSource
    {
        Game,
        Local
    }
}
