using System.Collections.Concurrent;
using System.Buffers;
using System.Buffers.Binary;
using System.Globalization;
using System.IO;
using System.Runtime.InteropServices;
using OtterTex;
using OtterImage = OtterTex.Image;
using LightlessSync.LightlessConfiguration;
using LightlessSync.FileCache;
using Microsoft.Extensions.Logging;
using Lumina.Data.Files;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;

/* 
 * OtterTex made by Ottermandias
 * thank you!!
*/

namespace LightlessSync.Services.TextureCompression;

public sealed class TextureDownscaleService
{
    private const int DefaultTargetMaxDimension = 2048;
    private const int MaxSupportedTargetDimension = 8192;
    private const int BlockMultiple = 4;

    private readonly ILogger<TextureDownscaleService> _logger;
    private readonly LightlessConfigService _configService;
    private readonly PlayerPerformanceConfigService _playerPerformanceConfigService;
    private readonly FileCacheManager _fileCacheManager;

    private readonly ConcurrentDictionary<string, Task> _activeJobs = new(StringComparer.OrdinalIgnoreCase);
    private readonly ConcurrentDictionary<string, string> _downscaledPaths = new(StringComparer.OrdinalIgnoreCase);
    private readonly SemaphoreSlim _downscaleSemaphore = new(4);
    private static readonly IReadOnlyDictionary<int, TextureCompressionTarget> BlockCompressedFormatMap =
        new Dictionary<int, TextureCompressionTarget>
        {
            [70] = TextureCompressionTarget.BC1, // DXGI_FORMAT_BC1_TYPELESS
            [71] = TextureCompressionTarget.BC1, // DXGI_FORMAT_BC1_UNORM
            [72] = TextureCompressionTarget.BC1, // DXGI_FORMAT_BC1_UNORM_SRGB

            [73] = TextureCompressionTarget.BC3, // DXGI_FORMAT_BC2_TYPELESS
            [74] = TextureCompressionTarget.BC3, // DXGI_FORMAT_BC2_UNORM
            [75] = TextureCompressionTarget.BC3, // DXGI_FORMAT_BC2_UNORM_SRGB
            [76] = TextureCompressionTarget.BC3, // DXGI_FORMAT_BC3_TYPELESS
            [77] = TextureCompressionTarget.BC3, // DXGI_FORMAT_BC3_UNORM
            [78] = TextureCompressionTarget.BC3, // DXGI_FORMAT_BC3_UNORM_SRGB

            [79] = TextureCompressionTarget.BC4, // DXGI_FORMAT_BC4_TYPELESS
            [80] = TextureCompressionTarget.BC4, // DXGI_FORMAT_BC4_UNORM
            [81] = TextureCompressionTarget.BC4, // DXGI_FORMAT_BC4_SNORM

            [82] = TextureCompressionTarget.BC5, // DXGI_FORMAT_BC5_TYPELESS
            [83] = TextureCompressionTarget.BC5, // DXGI_FORMAT_BC5_UNORM
            [84] = TextureCompressionTarget.BC5, // DXGI_FORMAT_BC5_SNORM

            [94] = TextureCompressionTarget.BC7, // DXGI_FORMAT_BC6H_TYPELESS (treated as BC7 for block detection)
            [95] = TextureCompressionTarget.BC7, // DXGI_FORMAT_BC6H_UF16
            [96] = TextureCompressionTarget.BC7, // DXGI_FORMAT_BC6H_SF16
            [97] = TextureCompressionTarget.BC7, // DXGI_FORMAT_BC7_TYPELESS
            [98] = TextureCompressionTarget.BC7, // DXGI_FORMAT_BC7_UNORM
            [99] = TextureCompressionTarget.BC7, // DXGI_FORMAT_BC7_UNORM_SRGB
        };

    public TextureDownscaleService(
        ILogger<TextureDownscaleService> logger,
        LightlessConfigService configService,
        PlayerPerformanceConfigService playerPerformanceConfigService,
        FileCacheManager fileCacheManager)
    {
        _logger = logger;
        _configService = configService;
        _playerPerformanceConfigService = playerPerformanceConfigService;
        _fileCacheManager = fileCacheManager;
    }

    public void ScheduleDownscale(string hash, string filePath, TextureMapKind mapKind)
    {
        if (!filePath.EndsWith(".tex", StringComparison.OrdinalIgnoreCase)) return;
        if (_activeJobs.ContainsKey(hash)) return;

        _activeJobs[hash] = Task.Run(async () =>
        {
            await DownscaleInternalAsync(hash, filePath, mapKind).ConfigureAwait(false);
        }, CancellationToken.None);
    }

    public string GetPreferredPath(string hash, string originalPath)
    {
        if (_downscaledPaths.TryGetValue(hash, out var existing) && File.Exists(existing))
        {
            return existing;
        }

        var resolved = GetExistingDownscaledPath(hash);
        if (!string.IsNullOrEmpty(resolved))
        {
            _downscaledPaths[hash] = resolved;
            return resolved;
        }

        return originalPath;
    }

    public Task WaitForPendingJobsAsync(IEnumerable<string>? hashes, CancellationToken token)
    {
        if (hashes is null)
        {
            return Task.CompletedTask;
        }

        var pending = new List<Task>();
        var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);

        foreach (var hash in hashes)
        {
            if (string.IsNullOrEmpty(hash) || !seen.Add(hash))
            {
                continue;
            }

            if (_activeJobs.TryGetValue(hash, out var job))
            {
                pending.Add(job);
            }
        }

        if (pending.Count == 0)
        {
            return Task.CompletedTask;
        }

        return Task.WhenAll(pending).WaitAsync(token);
    }

    private async Task DownscaleInternalAsync(string hash, string sourcePath, TextureMapKind mapKind)
    {
        TexHeaderInfo? headerInfo = null;
        string? destination = null;
        int targetMaxDimension = 0;
        bool onlyDownscaleUncompressed = false;
        bool? isIndexTexture = null;

        await _downscaleSemaphore.WaitAsync().ConfigureAwait(false);
        try
        {
            if (!File.Exists(sourcePath))
            {
                _logger.LogWarning("Cannot downscale texture {Hash}; source path missing: {Path}", hash, sourcePath);
                return;
            }

            headerInfo = TryReadTexHeader(sourcePath, out var header)
                ? header
                : (TexHeaderInfo?)null;
            var performanceConfig = _playerPerformanceConfigService.Current;
            targetMaxDimension = ResolveTargetMaxDimension();
            onlyDownscaleUncompressed = performanceConfig.OnlyDownscaleUncompressedTextures;

            destination = Path.Combine(GetDownscaledDirectory(), $"{hash}.tex");
            if (File.Exists(destination))
            {
                RegisterDownscaledTexture(hash, sourcePath, destination);
                return;
            }

            var indexTexture = IsIndexMap(mapKind);
            isIndexTexture = indexTexture;
            if (!indexTexture)
            {
                if (performanceConfig.EnableNonIndexTextureMipTrim
                    && await TryDropTopMipAsync(hash, sourcePath, destination, targetMaxDimension, onlyDownscaleUncompressed, headerInfo).ConfigureAwait(false))
                {
                    return;
                }

                if (!performanceConfig.EnableNonIndexTextureMipTrim)
                {
                    _logger.LogTrace("Skipping mip trim for non-index texture {Hash}; feature disabled.", hash);
                }

                _downscaledPaths[hash] = sourcePath;
                _logger.LogTrace("Skipping downscale for non-index texture {Hash}; no mip reduction required.", hash);
                return;
            }

            if (!performanceConfig.EnableIndexTextureDownscale)
            {
                _downscaledPaths[hash] = sourcePath;
                _logger.LogTrace("Skipping downscale for index texture {Hash}; feature disabled.", hash);
                return;
            }

            if (headerInfo is { } headerValue &&
                headerValue.Width <= targetMaxDimension &&
                headerValue.Height <= targetMaxDimension)
            {
                _downscaledPaths[hash] = sourcePath;
                _logger.LogTrace("Skipping downscale for index texture {Hash}; header dimensions {Width}x{Height} within target.", hash, headerValue.Width, headerValue.Height);
                return;
            }

            if (onlyDownscaleUncompressed && headerInfo.HasValue && IsBlockCompressedFormat(headerInfo.Value.Format))
            {
                _downscaledPaths[hash] = sourcePath;
                _logger.LogTrace("Skipping downscale for index texture {Hash}; block compressed format {Format}.", hash, headerInfo.Value.Format);
                return;
            }

            using var sourceScratch = TexFileHelper.Load(sourcePath);
            using var rgbaScratch = sourceScratch.GetRGBA(out var rgbaInfo).ThrowIfError(rgbaInfo);

            var bytesPerPixel = rgbaInfo.Meta.Format.BitsPerPixel() / 8;
            var width = rgbaInfo.Meta.Width;
            var height = rgbaInfo.Meta.Height;
            var requiredLength = width * height * bytesPerPixel;

            var rgbaPixels = rgbaScratch.Pixels.Slice(0, requiredLength);
            using var originalImage = SixLabors.ImageSharp.Image.LoadPixelData<Rgba32>(rgbaPixels, width, height);

            var targetSize = CalculateTargetSize(originalImage.Width, originalImage.Height, targetMaxDimension);
            if (targetSize.width == originalImage.Width && targetSize.height == originalImage.Height)
            {
                _downscaledPaths[hash] = sourcePath;
                _logger.LogTrace("Skipping downscale for index texture {Hash}; already within bounds.", hash);
                return;
            }

            using var resized = IndexDownscaler.Downscale(originalImage, targetSize.width, targetSize.height, BlockMultiple);

            using var resizedScratch = CreateScratchImage(resized, targetSize.width, targetSize.height);
            using var finalScratch = resizedScratch.Convert(DXGIFormat.B8G8R8A8UNorm);

            TexFileHelper.Save(destination, finalScratch);
            RegisterDownscaledTexture(hash, sourcePath, destination);
        }
        catch (Exception ex)
        {
            TryDelete(destination);
            _logger.LogWarning(
                ex,
                "Texture downscale failed for {Hash} ({MapKind}) from {SourcePath} -> {Destination}. TargetMax={TargetMaxDimension}, OnlyUncompressed={OnlyDownscaleUncompressed}, IsIndex={IsIndexTexture}, HeaderFormat={HeaderFormat}",
                hash,
                mapKind,
                sourcePath,
                destination ?? "<unresolved>",
                targetMaxDimension,
                onlyDownscaleUncompressed,
                isIndexTexture,
                headerInfo?.Format);
        }
        finally
        {
            _downscaleSemaphore.Release();
            _activeJobs.TryRemove(hash, out _);
        }
    }

    private static (int width, int height) CalculateTargetSize(int width, int height, int targetMaxDimension)
    {
        var resultWidth = width;
        var resultHeight = height;

        while (Math.Max(resultWidth, resultHeight) > targetMaxDimension)
        {
            resultWidth = Math.Max(BlockMultiple, resultWidth / 2);
            resultHeight = Math.Max(BlockMultiple, resultHeight / 2);
        }

        return (resultWidth, resultHeight);
    }

    private static ScratchImage CreateScratchImage(Image<Rgba32> image, int width, int height)
    {
        const int BytesPerPixel = 4;
        var requiredLength = width * height * BytesPerPixel;

        static ScratchImage Create(ReadOnlySpan<byte> pixels, int width, int height)
        {
            var scratchResult = ScratchImage.FromRGBA(pixels, width, height, out var creationInfo);
            return scratchResult.ThrowIfError(creationInfo);
        }

        if (image.DangerousTryGetSinglePixelMemory(out var pixelMemory))
        {
            var byteSpan = MemoryMarshal.AsBytes(pixelMemory.Span);
            if (byteSpan.Length < requiredLength)
            {
                throw new InvalidOperationException($"Image buffer shorter than expected ({byteSpan.Length} < {requiredLength}).");
            }

            return Create(byteSpan.Slice(0, requiredLength), width, height);
        }

        var rented = ArrayPool<byte>.Shared.Rent(requiredLength);
        try
        {
            var rentedSpan = rented.AsSpan(0, requiredLength);
            image.CopyPixelDataTo(rentedSpan);
            return Create(rentedSpan, width, height);
        }
        finally
        {
            ArrayPool<byte>.Shared.Return(rented);
        }
    }

    private static bool IsIndexMap(TextureMapKind kind)
        => kind is TextureMapKind.Mask
            or TextureMapKind.Index;

    private Task<bool> TryDropTopMipAsync(
        string hash,
        string sourcePath,
        string destination,
        int targetMaxDimension,
        bool onlyDownscaleUncompressed,
        TexHeaderInfo? headerInfo = null)
    {
        TexHeaderInfo? header = headerInfo;
        int dropCount = -1;
        int originalWidth = 0;
        int originalHeight = 0;
        int originalMipLevels = 0;

        try
        {
            if (!File.Exists(sourcePath))
            {
                _logger.LogWarning("Cannot trim mip levels for texture {Hash}; source path missing: {Path}", hash, sourcePath);
                return Task.FromResult(false);
            }

            if (header is null && TryReadTexHeader(sourcePath, out var discoveredHeader))
            {
                header = discoveredHeader;
            }

            if (header is TexHeaderInfo info)
            {
                if (onlyDownscaleUncompressed && IsBlockCompressedFormat(info.Format))
                {
                    _logger.LogTrace("Skipping mip trim for texture {Hash}; block compressed format {Format}.", hash, info.Format);
                    return Task.FromResult(false);
                }

                if (info.MipLevels <= 1)
                {
                    return Task.FromResult(false);
                }

                var headerDepth = info.Depth == 0 ? 1 : info.Depth;
                if (!ShouldTrimDimensions(info.Width, info.Height, headerDepth, targetMaxDimension))
                {
                    return Task.FromResult(false);
                }
            }

            using var original = TexFileHelper.Load(sourcePath);
            var meta = original.Meta;
            originalWidth = meta.Width;
            originalHeight = meta.Height;
            originalMipLevels = meta.MipLevels;
            if (meta.MipLevels <= 1)
            {
                return Task.FromResult(false);
            }

            if (!ShouldTrim(meta, targetMaxDimension))
            {
                return Task.FromResult(false);
            }

            var targetSize = CalculateTargetSize(meta.Width, meta.Height, targetMaxDimension);
            dropCount = CalculateDropCount(meta, targetSize.width, targetSize.height);
            if (dropCount <= 0)
            {
                return Task.FromResult(false);
            }

            using var trimmed = TrimMipChain(original, dropCount);
            TexFileHelper.Save(destination, trimmed);
            RegisterDownscaledTexture(hash, sourcePath, destination);
            _logger.LogDebug("Trimmed {DropCount} top mip level(s) for texture {Hash} -> {Path}", dropCount, hash, destination);
            return Task.FromResult(true);
        }
        catch (Exception ex)
        {
            _logger.LogWarning(
                ex,
                "Failed to trim mips for texture {Hash} from {SourcePath} -> {Destination}. TargetMax={TargetMaxDimension}, OnlyUncompressed={OnlyDownscaleUncompressed}, HeaderFormat={HeaderFormat}, OriginalSize={OriginalWidth}x{OriginalHeight}, OriginalMipLevels={OriginalMipLevels}, DropAttempt={DropCount}",
                hash,
                sourcePath,
                destination,
                targetMaxDimension,
                onlyDownscaleUncompressed,
                header?.Format,
                originalWidth,
                originalHeight,
                originalMipLevels,
                dropCount);
            TryDelete(destination);
            return Task.FromResult(false);
        }
    }

    private static int CalculateDropCount(in TexMeta meta, int targetWidth, int targetHeight)
    {
        var drop = 0;
        var width = meta.Width;
        var height = meta.Height;

        while ((width > targetWidth || height > targetHeight) && drop + 1 < meta.MipLevels)
        {
            drop++;
            width = ReduceDimension(width);
            height = ReduceDimension(height);
        }

        return drop;
    }

    private static ScratchImage TrimMipChain(ScratchImage source, int dropCount)
    {
        var meta = source.Meta;
        var newMeta = meta;
        newMeta.MipLevels = meta.MipLevels - dropCount;
        newMeta.Width = ReduceDimension(meta.Width, dropCount);
        newMeta.Height = ReduceDimension(meta.Height, dropCount);
        if (meta.Dimension == TexDimension.Tex3D)
        {
            newMeta.Depth = ReduceDimension(meta.Depth, dropCount);
        }

        var result = ScratchImage.Initialize(newMeta);
        CopyMipChainData(source, result, dropCount, meta);
        return result;
    }

    private static unsafe void CopyMipChainData(ScratchImage source, ScratchImage destination, int dropCount, in TexMeta sourceMeta)
    {
        var destinationMeta = destination.Meta;
        var arraySize = Math.Max(1, sourceMeta.ArraySize);
        var isCube = sourceMeta.IsCubeMap;
        var isVolume = sourceMeta.Dimension == TexDimension.Tex3D;

        for (var item = 0; item < arraySize; item++)
        {
            for (var mip = 0; mip < destinationMeta.MipLevels; mip++)
            {
                var sourceMip = mip + dropCount;
                var sliceCount = GetSliceCount(sourceMeta, sourceMip, isCube, isVolume);

                for (var slice = 0; slice < sliceCount; slice++)
                {
                    var srcImage = source.GetImage(sourceMip, item, slice);
                    var dstImage = destination.GetImage(mip, item, slice);
                    CopyImage(srcImage, dstImage);
                }
            }
        }
    }

    private static int GetSliceCount(in TexMeta meta, int mip, bool isCube, bool isVolume)
    {
        if (isCube)
        {
            return 6;
        }

        if (isVolume)
        {
            return Math.Max(1, meta.Depth >> mip);
        }

        return 1;
    }

    private static unsafe void CopyImage(in OtterImage source, in OtterImage destination)
    {
        var srcPtr = (byte*)source.Pixels;
        var dstPtr = (byte*)destination.Pixels;
        var bytesToCopy = Math.Min(source.SlicePitch, destination.SlicePitch);
        Buffer.MemoryCopy(srcPtr, dstPtr, destination.SlicePitch, bytesToCopy);
    }

    private static int ReduceDimension(int value, int iterations)
    {
        var result = value;
        for (var i = 0; i < iterations; i++)
        {
            result = ReduceDimension(result);
        }

        return result;
    }

    private static int ReduceDimension(int value)
        => value <= 1 ? 1 : Math.Max(1, value / 2);

    private static bool ShouldTrim(in TexMeta meta, int targetMaxDimension)
    {
        var depth = meta.Dimension == TexDimension.Tex3D ? Math.Max(1, meta.Depth) : 1;
        return ShouldTrimDimensions(meta.Width, meta.Height, depth, targetMaxDimension);
    }

    private static bool ShouldTrimDimensions(int width, int height, int depth, int targetMaxDimension)
    {
        if (width <= targetMaxDimension && height <= targetMaxDimension && depth <= targetMaxDimension)
        {
            return false;
        }

        return true;
    }

    private int ResolveTargetMaxDimension()
    {
        var configured = _playerPerformanceConfigService.Current.TextureDownscaleMaxDimension;
        if (configured <= 0)
        {
            return DefaultTargetMaxDimension;
        }

        return Math.Clamp(configured, BlockMultiple, MaxSupportedTargetDimension);
    }

    private readonly struct TexHeaderInfo
    {
        public TexHeaderInfo(ushort width, ushort height, ushort depth, ushort mipLevels, TexFile.TextureFormat format)
        {
            Width = width;
            Height = height;
            Depth = depth;
            MipLevels = mipLevels;
            Format = format;
        }

        public ushort Width { get; }
        public ushort Height { get; }
        public ushort Depth { get; }
        public ushort MipLevels { get; }
        public TexFile.TextureFormat Format { get; }
    }

    private static bool TryReadTexHeader(string path, out TexHeaderInfo header)
    {
        header = default;

        try
        {
            Span<byte> buffer = stackalloc byte[16];
            using var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete);
            var read = stream.Read(buffer);
            if (read < buffer.Length)
            {
                return false;
            }

            var formatValue = BinaryPrimitives.ReadInt32LittleEndian(buffer[4..8]);
            var format = (TexFile.TextureFormat)formatValue;
            var width = BinaryPrimitives.ReadUInt16LittleEndian(buffer[8..10]);
            var height = BinaryPrimitives.ReadUInt16LittleEndian(buffer[10..12]);
            var depth = BinaryPrimitives.ReadUInt16LittleEndian(buffer[12..14]);
            var mipLevels = BinaryPrimitives.ReadUInt16LittleEndian(buffer[14..16]);
            header = new TexHeaderInfo(width, height, depth, mipLevels, format);
            return true;
        }
        catch
        {
            return false;
        }
    }

    private static bool IsBlockCompressedFormat(TexFile.TextureFormat format)
        => TryGetCompressionTarget(format, out _);

    private static bool TryGetCompressionTarget(TexFile.TextureFormat format, out TextureCompressionTarget target)
    {
        if (BlockCompressedFormatMap.TryGetValue(unchecked((int)format), out var mapped))
        {
            target = mapped;
            return true;
        }

        target = default;
        return false;
    }

    private void RegisterDownscaledTexture(string hash, string sourcePath, string destination)
    {
        _downscaledPaths[hash] = destination;
        _logger.LogDebug("Downscaled texture {Hash} -> {Path}", hash, destination);

        var performanceConfig = _playerPerformanceConfigService.Current;
        if (performanceConfig.KeepOriginalTextureFiles)
        {
            return;
        }

        if (string.Equals(sourcePath, destination, StringComparison.OrdinalIgnoreCase))
        {
            return;
        }

        if (!TryReplaceCacheEntryWithDownscaled(hash, sourcePath, destination))
        {
            return;
        }

        TryDelete(sourcePath);
    }

    private bool TryReplaceCacheEntryWithDownscaled(string hash, string sourcePath, string destination)
    {
        try
        {
            var cacheEntry = _fileCacheManager.GetFileCacheByHash(hash);
            if (cacheEntry is null || !cacheEntry.IsCacheEntry)
            {
                return File.Exists(sourcePath) ? false : true;
            }

            var cacheFolder = _configService.Current.CacheFolder;
            if (string.IsNullOrEmpty(cacheFolder))
            {
                return false;
            }

            if (!destination.StartsWith(cacheFolder, StringComparison.OrdinalIgnoreCase))
            {
                return false;
            }

            var info = new FileInfo(destination);
            if (!info.Exists)
            {
                return false;
            }

            var relative = Path.GetRelativePath(cacheFolder, destination)
                .Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar);
            var sanitizedRelative = relative.TrimStart(Path.DirectorySeparatorChar);
            var prefixed = Path.Combine(FileCacheManager.CachePrefix, sanitizedRelative);

            var replacement = new FileCacheEntity(
                hash,
                prefixed,
                info.LastWriteTimeUtc.Ticks.ToString(CultureInfo.InvariantCulture),
                info.Length,
                cacheEntry.CompressedSize);
            replacement.SetResolvedFilePath(destination);

            if (!string.Equals(cacheEntry.PrefixedFilePath, prefixed, StringComparison.OrdinalIgnoreCase))
            {
                _fileCacheManager.RemoveHashedFile(cacheEntry.Hash, cacheEntry.PrefixedFilePath);
            }

            _fileCacheManager.UpdateHashedFile(replacement, computeProperties: false);
            _fileCacheManager.WriteOutFullCsv();

            _logger.LogTrace("Replaced cache entry for texture {Hash} to downscaled path {Path}", hash, destination);
            return true;
        }
        catch (Exception ex)
        {
            _logger.LogTrace(ex, "Failed to replace cache entry for texture {Hash}", hash);
            return false;
        }
    }

    private string? GetExistingDownscaledPath(string hash)
    {
        var candidate = Path.Combine(GetDownscaledDirectory(), $"{hash}.tex");
        return File.Exists(candidate) ? candidate : null;
    }

    private string GetDownscaledDirectory()
    {
        var directory = Path.Combine(_configService.Current.CacheFolder, "downscaled");
        if (!Directory.Exists(directory))
        {
            try
            {
                Directory.CreateDirectory(directory);
            }
            catch (Exception ex)
            {
                _logger.LogTrace(ex, "Failed to create downscaled directory {Directory}", directory);
            }
        }

        return directory;
    }

    private static void TryDelete(string? path)
    {
        if (string.IsNullOrEmpty(path)) return;
        try
        {
            if (File.Exists(path))
            {
                File.Delete(path);
            }
        }
        catch
        {
            // ignored
        }
    }

}
