using CUE4Parse.Compression;
using CUE4Parse.Encryption.Aes;
using CUE4Parse.FileProvider;
using CUE4Parse.FileProvider.Objects;
using CUE4Parse.MappingsProvider.Usmap;
using CUE4Parse.UE4.Assets;
using CUE4Parse.UE4.Assets.Exports.Engine;
using CUE4Parse.UE4.Assets.Exports.Texture;
using CUE4Parse.UE4.Assets.Objects;
using CUE4Parse.UE4.Objects.Core.i18N;
using CUE4Parse.UE4.Objects.Core.Misc;
using CUE4Parse.UE4.Objects.UObject;
using CUE4Parse.UE4.Versions;
using CUE4Parse.UE4.VirtualFileSystem;
using CUE4Parse_Conversion.Textures;
using Newtonsoft.Json;

struct CharacterInfo
{
    public string name;
    public string codeName;
    public int idx;
    public string iconFilePath;
}

struct ItemInfo
{
    public string id;
    public string name;
    public string iconFilePath;
}

struct OfferingInfo
{
    public string id;
    public string name;
    public string iconFilePath;
    public EPlayerRole role;
}

struct PerkInfo
{
    public string id;
    public string name;
    public string iconFilePath;
    public EPlayerRole role;
}

struct DLCInfo
{
    public string id;
    public string? name;
    public Dictionary<string, string> dlcIds;
}

struct CustomizationItemInfo
{
    public string id;
    public string name;
    public string iconFilePath;
    public int associatedCharacter;
    public ECustomizationCategory category;
}

struct AddonInfo
{
    public string id { get; set; }
    public string name { get; set; }
    public string iconFilePath { get; set; }
    public EPlayerRole role { get; set; }
}
class Dumper
{
    private DefaultFileProvider? _provider;
    private IAesVfsReader? _dataPak;
    private Logger _log;

    private string _outDir;

    private readonly Dictionary<int, CharacterInfo> _characterMap = new();
    private readonly Dictionary<string, ItemInfo> _itemMap = new();
    private readonly Dictionary<string, AddonInfo> _addonMap = new();
    private readonly Dictionary<string, PerkInfo> _perkMap = new();
    private readonly Dictionary<string, OfferingInfo> _offeringMap = new();
    private readonly Dictionary<string, CustomizationItemInfo> _customizationItemMap = new();

    public Dumper(string outDir)
    {
        _log = new Logger("Dumper");

        if (string.IsNullOrWhiteSpace(outDir))
            throw new ArgumentException("Output directory cannot be null or empty.", nameof(outDir));

        string fullPath = Path.GetFullPath(outDir);
        if (!Directory.Exists(fullPath))
            Directory.CreateDirectory(fullPath);

        _outDir = fullPath;
    }

    public bool Init()
    {
        /*
         * mapping
         */
        string baseDir = AppDomain.CurrentDomain.BaseDirectory;
        string mappingPath = Path.GetFullPath(Path.Combine(baseDir, "mapping.usmap"));

        if (!File.Exists(mappingPath))
            return false;

        /*
         *  compression
         */
        ZlibHelper.Initialize();
        OodleHelper.Initialize(Path.Combine(baseDir, OodleHelper.OodleFileName));

        /*
         * game path
         */
        string? gamePath = Utils.GetGamePath();
        if (gamePath == null) return false;

        string pakDir = Path.Combine(gamePath, "DeadByDaylight", "Content", "Paks");
        if (!Directory.Exists(pakDir))
        {
            Console.WriteLine("PAK dir does not exist. (Invalid install?)");
            return false;
        }

        /*
         *  file provider
         */
        VersionContainer? version = new VersionContainer(EGame.GAME_DeadByDaylight, ETexturePlatform.DesktopMobile);
        _provider = new DefaultFileProvider(pakDir, SearchOption.TopDirectoryOnly, version)
        {
            MappingsContainer = new FileUsmapTypeMappingsProvider(mappingPath)
        };

        _provider.Initialize();
        _provider.SubmitKey(new FGuid(), new FAesKey(Constants.AESKey));
        _provider.Mount();
        _provider.PostMount();

        if (!_provider.TryGetArchive("pakchunk4-WinGDK.utoc", out _dataPak)) // data
        {
            Console.WriteLine("Failed to load pakchunk4-WinGDK.utoc");
            return false;
        }

        _provider.LoadLocalization();

        return true;
    }

    /*
     * dumping functions
     */
    public void DumpCharacters()
    {
        ProcessDataTables("/CharacterDescriptionDB.uasset", "characters", (rowKey, props) =>
        {
            if (!TryGetProp<int>(props, "CharacterIndex", out int charIndex))
                throw new KeyNotFoundException("CharacterIndex was not found.");

            if (charIndex == -1) return; // there is a -1 character (template, placeholder, spectator or smth)

            if (!TryGetStringProp<FName>(props, "CharacterCodename", out string codeName) || !TryGetStringProp<FText>(props, "DisplayName", out string charName) || !TryGetStringProp<FName>(props, "IconFilePath", out string charIconFilePath))
                throw new KeyNotFoundException($"Required properties missing for Character: {rowKey}");

            _characterMap[charIndex] = new CharacterInfo
            {
                name = charName,
                codeName = codeName,
                idx = charIndex,
                iconFilePath = charIconFilePath
            };
        });

        _log.Info("Dumped {0} characters", _characterMap.Count);
        WriteJson("characters", _characterMap.Values);
    }

    public void DumpItems()
    {
        ProcessDataTables("/ItemDB.uasset", "items", (rowKey, props) =>
        {
            if (!TryGetProp(props, "Type", out EInventoryItemType itemType)
                || !TryGetProp(props, "UIData", out FStructFallback uiDataFb)
                || !TryGetProp(props, "Role", out EPlayerRole role)
                || !TryGetProp(props, "Inventory", out bool isInventory)
                || !TryGetProp(props, "IsFakeItem", out bool isFakeItem))
                throw new KeyNotFoundException($"Required properties missing for Item: {rowKey}");

            UIDataStruct uiData = uiDataFb.MapToStruct<UIDataStruct>();

            if (isFakeItem || !isInventory || itemType != EInventoryItemType.Item || role != EPlayerRole.VE_Camper)
                return;

            if (uiData.IconAssetList.Length == 0)
                throw new InvalidDataException("Item's UIData had no icons");

            _itemMap[rowKey] = new ItemInfo
            {
                id = rowKey,
                name = uiData.DisplayName.ToString(),
                iconFilePath = uiData.IconAssetList[0].ToString()
            };
        });

        _log.Info("Dumped {0} items", _itemMap.Count);
        WriteJson("items", _itemMap.Values);
    }

    public void DumpAddons()
    {
        ProcessDataTables("/ItemAddonDB.uasset", "addons", (rowKey, props) =>
        {
            if (!TryGetProp(props, "Role", out EPlayerRole role)
                || !TryGetProp(props, "UIData", out FStructFallback uiDataFb)
                || !TryGetProp(props, "Inventory", out bool isInventory)
                || !TryGetProp(props, "IsFakeItem", out bool isFakeItem))
                throw new KeyNotFoundException($"Required properties missing for Addon: {rowKey}");

            UIDataStruct uiData = uiDataFb.MapToStruct<UIDataStruct>();

            if (!isInventory || isFakeItem)
                return;

            if (uiData.IconAssetList.Length == 0)
                throw new InvalidDataException("Addon's UIData had no icons");

            _addonMap[rowKey] = new AddonInfo
            {
                id = rowKey,
                name = uiData.DisplayName.ToString(),
                iconFilePath = uiData.IconAssetList[0].ToString(),
                role = role
            };
        });

        _log.Info("Dumped {0} addons", _addonMap.Count);
        WriteJson("addons", _addonMap.Values);
    }

    public void DumpPerks()
    {
        ProcessDataTables("/PerkDB.uasset", "perks", (rowKey, props) =>
        {
            if (!TryGetProp(props, "Role", out EPlayerRole role)
                || !TryGetProp(props, "UIData", out FStructFallback uiDataFb)
                || !TryGetProp(props, "Inventory", out bool isInventory)
                || !TryGetProp(props, "IsFakeItem", out bool isFakeItem))
                throw new KeyNotFoundException($"Required properties missing for Perk: {rowKey}");

            UIDataStruct uiData = uiDataFb.MapToStruct<UIDataStruct>();

            if (!isInventory || isFakeItem)
                return;

            if (uiData.IconAssetList.Length == 0)
                throw new InvalidDataException("Perk's UIData had no icons");

            _perkMap[rowKey] = new PerkInfo
            {
                id = rowKey,
                name = uiData.DisplayName.ToString(),
                iconFilePath = uiData.IconAssetList[0].ToString(),
                role = role
            };
        });

        _log.Info("Dumped {0} perks", _perkMap.Count);
        WriteJson("perks", _perkMap.Values);
    }

    public void DumpOfferings()
    {
        ProcessDataTables("/OfferingDB.uasset", "offerings", (rowKey, props) =>
        {
            if (!TryGetProp(props, "Role", out EPlayerRole role)
                || !TryGetProp(props, "UIData", out FStructFallback uiDataFb)
                || !TryGetProp(props, "Inventory", out bool isInventory)
                || !TryGetProp(props, "IsFakeItem", out bool isFakeItem))
                throw new KeyNotFoundException($"Required properties missing for offering: {rowKey}");

            UIDataStruct uiData = uiDataFb.MapToStruct<UIDataStruct>();

            if (!isInventory || isFakeItem)
                return;

            if (uiData.IconAssetList.Length == 0)
                throw new InvalidDataException("Offerings's UIData had no icons");

            _offeringMap[rowKey] = new OfferingInfo
            {
                id = rowKey,
                name = uiData.DisplayName.ToString(),
                iconFilePath = uiData.IconAssetList[0].ToString(),
                role = role
            };
        });

        _log.Info("Dumped {0} offerings", _offeringMap.Count);
        WriteJson("offerings", _offeringMap.Values);
    }

    public void DumpDLCs()
    {
        List<DLCInfo> dlcList = new List<DLCInfo>();

        ProcessDataTables("/DlcDB.uasset", "dlcs", (rowKey, props) =>
        {
            if (!TryGetProp(props, "DlcIdSteam", out string steamId)
                || !TryGetProp(props, "DlcIdEpic", out string epicId)
                || !TryGetProp(props, "DlcIdGRDK", out string grdkId)
                )
                throw new KeyNotFoundException($"Required properties missing for DLC: {rowKey}");


            string? displayName = null;
            if (TryGetStringProp<FText>(props, "DisplayName", out string foundName) && !string.IsNullOrWhiteSpace(foundName))
                displayName = foundName;

            DLCInfo info = new DLCInfo
            {
                id = rowKey,
                name = displayName,
                dlcIds = new Dictionary<string, string> { { "steam", steamId }, { "epic", epicId }, { "grdk", grdkId } }
            };

            dlcList.Add(info);
        });

        _log.Info("Dumped {0} dlcs", dlcList.Count);
        WriteJson("dlcs", dlcList.ToArray());
    }

    public void DumpCustomizations()
    {
        ProcessDataTables("/CustomizationItemDB.uasset", "customization items", (rowKey, props) =>
        {
            if (!TryGetProp(props, "Category", out ECustomizationCategory category)
            || !TryGetProp(props, "Availability", out FStructFallback availabilityFb)
            || !TryGetProp(props, "UIData", out FStructFallback uiDataFb)
            || !TryGetProp(props, "AssociatedCharacter", out int associatedCharacter))
                throw new KeyNotFoundException($"Required properties missing for CustomizationItem: {rowKey}");

            AvailabilityStruct availability = availabilityFb.MapToStruct<AvailabilityStruct>();
            UIDataStruct uiData = uiDataFb.MapToStruct<UIDataStruct>();

            if (availability.DLCId == "development") return; // skip development dlc items just in case

            if (uiData.IconAssetList.Length == 0)
                throw new InvalidDataException("CustomizationItem's UIData had no icons");

            string iconPathStr = uiData.IconAssetList[0].ToString();

            _customizationItemMap[rowKey] = new CustomizationItemInfo
            {
                id = rowKey,
                name = uiData.DisplayName.ToString(),
                associatedCharacter = associatedCharacter,
                category = category,
                iconFilePath = iconPathStr,
            };
        });

        _log.Info("Dumped {0} customization items", _customizationItemMap.Count);
        WriteJson("customization_items", _customizationItemMap.Values);
    }

    public void DumpCharacterIcons() => ExportIcons(_characterMap.Values.Select(x => x.iconFilePath), "character", "/character-icons/");
    public void DumpItemIcons() => ExportIcons(_itemMap.Values.Select(x => x.iconFilePath), "item", "/item-icons/");
    public void DumpAddonIcons() => ExportIcons(_addonMap.Values.Select(x => x.iconFilePath), "addon", "/addon-icons/");
    public void DumpPerkIcons() => ExportIcons(_perkMap.Values.Select(x => x.iconFilePath), "perk", "/perk-icons/");
    public void DumpOfferingIcons() => ExportIcons(_offeringMap.Values.Select(x => x.iconFilePath), "offering", "/offering-icons/");
    public void DumpCustomizationIcons()
    {
        _log.Info("Dumping customization item icons");

        foreach (var item in _customizationItemMap.Values)
        {
            string outPath = "customization/";

            if (item.category == ECustomizationCategory.Charm ||
                item.category == ECustomizationCategory.Badge ||
                item.category == ECustomizationCategory.Banner ||
                item.category == ECustomizationCategory.PortraitBackground)
            {
                switch (item.category)
                {
                    case ECustomizationCategory.Charm:
                        outPath += "charms/";
                        break;
                    case ECustomizationCategory.Badge:
                        outPath += "badges/";
                        break;
                    case ECustomizationCategory.Banner:
                        outPath += "banners/";
                        break;
                    case ECustomizationCategory.PortraitBackground:
                        outPath += "portrait-backgrounds/";
                        break;
                }
            }
            else if (item.associatedCharacter > -1)
            {
                string charFolderName = item.associatedCharacter.ToString();

                if (_characterMap.TryGetValue(item.associatedCharacter, out CharacterInfo charInfo) && !string.IsNullOrWhiteSpace(charInfo.name))
                    charFolderName = string.Join("_", charInfo.name.Split(Path.GetInvalidFileNameChars()));

                outPath += $"characters/{charFolderName}/";

                switch (item.category)
                {
                    case ECustomizationCategory.SurvivorHead:
                    case ECustomizationCategory.KillerHead:
                        outPath += "heads/";
                        break;
                    case ECustomizationCategory.SurvivorTorso:
                        outPath += "torsos/";
                        break;
                    case ECustomizationCategory.SurvivorLegs:
                        outPath += "legs/";
                        break;
                    case ECustomizationCategory.KillerBody:
                        outPath += "bodys/";
                        break;
                    case ECustomizationCategory.KillerWeapon:
                        outPath += "weapons/";
                        break;
                    case ECustomizationCategory.Outfits:
                        outPath += "outfits/";
                        break;
                }
            }

            try
            {
                ExportIcon(item.iconFilePath, outPath);
            }
            catch (Exception ex)
            {
                _log.Error($"Failed to export icon for {item.id}: {ex.Message}");
            }
        }

        _log.Info("Dumped all customization item icons");
    }

    /*
     * bulk functions for dumping
     */
    private void ProcessDataTables(string pathFilter, string logName, Action<string, List<FPropertyTag>> rowProcessor)
    {
        if (_dataPak == null || _provider == null)
            throw new InvalidOperationException("Attempted to call dump function without dumper initialization/state");

        _log.Info($"Dumping {logName}");

        var dbPaths = _dataPak.Files.Keys.Where(x => x.Contains(pathFilter, StringComparison.OrdinalIgnoreCase));

        foreach (string path in dbPaths)
        {
            string cleanPath = path.Contains('.') ? path[..path.LastIndexOf('.')] : path;

            if (_provider.TryLoadPackageObject<UDataTable>(cleanPath, out UDataTable? dataTable))
            {
                foreach (KeyValuePair<FName, FStructFallback> row in dataTable.RowMap)
                    rowProcessor(row.Key.Text, row.Value.Properties);
            }
        }
    }

    private void ExportIcons(IEnumerable<string> iconPaths, string logName, string outFolder)
    {
        if (_provider == null)
            throw new InvalidOperationException("Attempted to call dump function without dumper initialization/state");

        _log.Info($"Dumping {logName} icons");

        foreach (string path in iconPaths)
            ExportIcon(path, outFolder);

        _log.Info($"Dumped all {logName} icons");
    }

    /*
     * internal helper functions
     */
    private bool TryGetProp<T>(IEnumerable<FPropertyTag> properties, string propName, out T value)
    {
        FPropertyTag? prop = properties.FirstOrDefault(p => p.Name.Text == propName);
        if (prop != null && prop.Tag != null)
        {
            T? val = prop.Tag.GetValue<T>();
            if (val != null)
            {
                value = val;
                return true;
            }
        }
        _log.Error("Character missing or invalid property: {0}", propName);
        value = default!;
        return false;
    }

    private bool TryGetStringProp<T>(IEnumerable<FPropertyTag> properties, string propName, out string value)
    {
        if (TryGetProp<T>(properties, propName, out T? rawValue) && rawValue != null)
        {
            string? strVal = rawValue.ToString();

            if (!string.IsNullOrEmpty(strVal))
            {
                value = strVal;
                return true;
            }
        }

        //_log.Error($"Property '{propName}' returned an empty or null string.");
        value = string.Empty;
        return false;
    }

    private void WriteJson(string name, object data)
    {
        string json = JsonConvert.SerializeObject(data, Formatting.Indented);
        File.WriteAllText(_outDir + name + ".json", json);
    }

    private void ExportIcon(string assetPath, string outPath)
    {
        if (_provider == null)
            throw new InvalidOperationException("Attempted to call dump function without dumper initialization/state");

        string cleanPath = assetPath.Contains('.') ? assetPath[..assetPath.LastIndexOf('.')] : assetPath;
        string fileName = Path.GetFileName(cleanPath) + ".png";
        string relativeOutPath = outPath.TrimStart('/', '\\');
        string fullPath = Path.Combine(_outDir, relativeOutPath, fileName);

        if (cleanPath.StartsWith("UI/Icons/"))
            cleanPath = "/Game/UI/UMGAssets/" + cleanPath["UI/".Length..];

        if (File.Exists(fullPath)) return;

        /*
         * shoutout to whichever dev kept fucking up the casing
         * its rly surprising how many times this happened
         */
        if (!_provider.TryLoadPackage(cleanPath, out IPackage? package))
        {
            string[] pathSegments = cleanPath.Split('/', StringSplitOptions.RemoveEmptyEntries);

            if (pathSegments.Length > 1)
            {
                string innerPath = string.Join('/', pathSegments.Skip(1)) + ".uasset";

                KeyValuePair<string, GameFile> actualFile = _provider.Files.FirstOrDefault(
                    kvp => kvp.Key.EndsWith(innerPath, StringComparison.OrdinalIgnoreCase)
                );

                if (actualFile.Value != null)
                    _provider.TryLoadPackage(actualFile.Value, out package);
            }
        }


        if (package != null)
        {
            UTexture2D? texture = package.GetExports().OfType<UTexture2D>().FirstOrDefault();

            if (texture != null)
            {
                CTexture? bitmap = texture.Decode(ETexturePlatform.DesktopMobile);

                if (bitmap != null)
                {
                    byte[] bytes = bitmap.Encode(ETextureFormat.Png, false, out _);

                    string? parentFolder = Path.GetDirectoryName(fullPath);
                    if (!string.IsNullOrEmpty(parentFolder))
                        Directory.CreateDirectory(parentFolder);

                    File.WriteAllBytes(fullPath, bytes);
                }
                else
                    throw new InvalidDataException("Bitmap was invalid");
            }
            else
                throw new FileNotFoundException("Failed to find texture");
        }
        else
            throw new FileNotFoundException("Failed to find texture package");
    }
}
