using ICSharpCode.SharpZipLib.Zip;
using Ryujinx.Common;
using Ryujinx.Common.Logging;
using Ryujinx.Graphics.GAL;
using Ryujinx.Graphics.Gpu.Shader.Cache.Definition;
using System;
using System.Collections.Generic;
using System.IO;

namespace Ryujinx.Graphics.Gpu.Shader.Cache
{
    /// <summary>
    /// Class handling shader cache migrations.
    /// </summary>
    static class CacheMigration
    {
        /// <summary>
        /// Check if the given cache version need to recompute its hash.
        /// </summary>
        /// <param name="version">The version in use</param>
        /// <param name="newVersion">The new version after migration</param>
        /// <returns>True if a hash recompute is needed</returns>
        public static bool NeedHashRecompute(ulong version, out ulong newVersion)
        {
            const ulong TargetBrokenVersion = 1717;
            const ulong TargetFixedVersion = 1759;

            newVersion = TargetFixedVersion;

            if (version == TargetBrokenVersion)
            {
                return true;
            }

            return false;
        }

        private class StreamZipEntryDataSource : IStaticDataSource
        {
            private readonly ZipFile Archive;
            private readonly ZipEntry Entry;
            public StreamZipEntryDataSource(ZipFile archive, ZipEntry entry)
            {
                Archive = archive;
                Entry = entry;
            }

            public Stream GetSource()
            {
                return Archive.GetInputStream(Entry);
            }
        }

        /// <summary>
        /// Move a file with the name of a given hash to another in the cache archive.
        /// </summary>
        /// <param name="archive">The archive in use</param>
        /// <param name="oldKey">The old key</param>
        /// <param name="newKey">The new key</param>
        private static void MoveEntry(ZipFile archive, Hash128 oldKey, Hash128 newKey)
        {
            ZipEntry oldGuestEntry = archive.GetEntry($"{oldKey}");

            if (oldGuestEntry != null)
            {
                archive.Add(new StreamZipEntryDataSource(archive, oldGuestEntry), $"{newKey}", CompressionMethod.Deflated);
                archive.Delete(oldGuestEntry);
            }
        }

        /// <summary>
        /// Recompute all the hashes of a given cache.
        /// </summary>
        /// <param name="guestBaseCacheDirectory">The guest cache directory path</param>
        /// <param name="hostBaseCacheDirectory">The host cache directory path</param>
        /// <param name="graphicsApi">The graphics api in use</param>
        /// <param name="hashType">The hash type in use</param>
        /// <param name="newVersion">The version to write in the host and guest manifest after migration</param>
        private static void RecomputeHashes(string guestBaseCacheDirectory, string hostBaseCacheDirectory, CacheGraphicsApi graphicsApi, CacheHashType hashType, ulong newVersion)
        {
            string guestManifestPath = CacheHelper.GetManifestPath(guestBaseCacheDirectory);
            string hostManifestPath = CacheHelper.GetManifestPath(hostBaseCacheDirectory);

            if (CacheHelper.TryReadManifestFile(guestManifestPath, CacheGraphicsApi.Guest, hashType, out _, out HashSet<Hash128> guestEntries))
            {
                CacheHelper.TryReadManifestFile(hostManifestPath, graphicsApi, hashType, out _, out HashSet<Hash128> hostEntries);

                Logger.Info?.Print(LogClass.Gpu, "Shader cache hashes need to be recomputed, performing migration...");

                string guestArchivePath = CacheHelper.GetArchivePath(guestBaseCacheDirectory);
                string hostArchivePath = CacheHelper.GetArchivePath(hostBaseCacheDirectory);

                ZipFile guestArchive = new ZipFile(File.Open(guestArchivePath, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None));
                ZipFile hostArchive = new ZipFile(File.Open(hostArchivePath, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None));

                CacheHelper.EnsureArchiveUpToDate(guestBaseCacheDirectory, guestArchive, guestEntries);
                CacheHelper.EnsureArchiveUpToDate(hostBaseCacheDirectory, hostArchive, hostEntries);

                int programIndex = 0;

                HashSet<Hash128> newEntries = new HashSet<Hash128>();

                foreach (Hash128 oldHash in guestEntries)
                {
                    byte[] guestProgram = CacheHelper.ReadFromArchive(guestArchive, oldHash);

                    Logger.Info?.Print(LogClass.Gpu, $"Migrating shader {oldHash} ({programIndex + 1} / {guestEntries.Count})");

                    if (guestProgram != null)
                    {
                        ReadOnlySpan<byte> guestProgramReadOnlySpan = guestProgram;

                        ReadOnlySpan<GuestShaderCacheEntry> cachedShaderEntries = GuestShaderCacheEntry.Parse(ref guestProgramReadOnlySpan, out GuestShaderCacheHeader fileHeader);

                        TransformFeedbackDescriptor[] tfd = CacheHelper.ReadTransformFeedbackInformation(ref guestProgramReadOnlySpan, fileHeader);

                        Hash128 newHash = CacheHelper.ComputeGuestHashFromCache(cachedShaderEntries, tfd);

                        if (newHash != oldHash)
                        {
                            MoveEntry(guestArchive, oldHash, newHash);
                            MoveEntry(hostArchive, oldHash, newHash);
                        }
                        else
                        {
                            Logger.Warning?.Print(LogClass.Gpu, $"Same hashes for shader {oldHash}");
                        }

                        newEntries.Add(newHash);
                    }

                    programIndex++;
                }

                byte[] newGuestManifestContent = CacheHelper.ComputeManifest(newVersion, CacheGraphicsApi.Guest, hashType, newEntries);
                byte[] newHostManifestContent = CacheHelper.ComputeManifest(newVersion, graphicsApi, hashType, newEntries);

                File.WriteAllBytes(guestManifestPath, newGuestManifestContent);
                File.WriteAllBytes(hostManifestPath, newHostManifestContent);

                guestArchive.CommitUpdate();
                hostArchive.CommitUpdate();

                guestArchive.Close();
                hostArchive.Close();
            }
        }

        /// <summary>
        /// Check and run cache migration if needed.
        /// </summary>
        /// <param name="baseCacheDirectory">The base path of the cache</param>
        /// <param name="graphicsApi">The graphics api in use</param>
        /// <param name="hashType">The hash type in use</param>
        /// <param name="shaderProvider">The shader provider name of the cache</param>
        public static void Run(string baseCacheDirectory, CacheGraphicsApi graphicsApi, CacheHashType hashType, string shaderProvider)
        {
            string guestBaseCacheDirectory = CacheHelper.GenerateCachePath(baseCacheDirectory, CacheGraphicsApi.Guest, "", "program");
            string hostBaseCacheDirectory = CacheHelper.GenerateCachePath(baseCacheDirectory, graphicsApi, shaderProvider, "host");

            string guestArchivePath = CacheHelper.GetArchivePath(guestBaseCacheDirectory);
            string hostArchivePath = CacheHelper.GetArchivePath(hostBaseCacheDirectory);

            bool isReadOnly = CacheHelper.IsArchiveReadOnly(guestArchivePath) || CacheHelper.IsArchiveReadOnly(hostArchivePath);

            if (!isReadOnly && CacheHelper.TryReadManifestHeader(CacheHelper.GetManifestPath(guestBaseCacheDirectory), out CacheManifestHeader header))
            {
                if (NeedHashRecompute(header.Version, out ulong newVersion))
                {
                    RecomputeHashes(guestBaseCacheDirectory, hostBaseCacheDirectory, graphicsApi, hashType, newVersion);
                }
            }
        }
    }
}