using Ryujinx.Common;
using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.CompilerServices;

namespace Ryujinx.Graphics.Gpu.Shader.DiskCache
{
    /// <summary>
    /// On-disk shader cache storage for guest code.
    /// </summary>
    class DiskCacheGuestStorage
    {
        private const uint TocMagic = (byte)'T' | ((byte)'O' << 8) | ((byte)'C' << 16) | ((byte)'G' << 24);

        private const ushort VersionMajor = 1;
        private const ushort VersionMinor = 1;
        private const uint VersionPacked = ((uint)VersionMajor << 16) | VersionMinor;

        private const string TocFileName = "guest.toc";
        private const string DataFileName = "guest.data";

        private readonly string _basePath;

        /// <summary>
        /// TOC (Table of contents) file header.
        /// </summary>
        private struct TocHeader
        {
            /// <summary>
            /// Magic value, for validation and identification purposes.
            /// </summary>
            public uint Magic;

            /// <summary>
            /// File format version.
            /// </summary>
            public uint Version;

            /// <summary>
            /// Header padding.
            /// </summary>
            public uint Padding;

            /// <summary>
            /// Number of modifications to the file, also the shaders count.
            /// </summary>
            public uint ModificationsCount;

            /// <summary>
            /// Reserved space, to be used in the future. Write as zero.
            /// </summary>
            public ulong Reserved;

            /// <summary>
            /// Reserved space, to be used in the future. Write as zero.
            /// </summary>
            public ulong Reserved2;
        }

        /// <summary>
        /// TOC (Table of contents) file entry.
        /// </summary>
        private struct TocEntry
        {
            /// <summary>
            /// Offset of the data on the data file.
            /// </summary>
            public uint Offset;

            /// <summary>
            /// Code size.
            /// </summary>
            public uint CodeSize;

            /// <summary>
            /// Constant buffer 1 data size.
            /// </summary>
            public uint Cb1DataSize;

            /// <summary>
            /// Hash of the code and constant buffer data.
            /// </summary>
            public uint Hash;
        }

        /// <summary>
        /// TOC (Table of contents) memory cache entry.
        /// </summary>
        private struct TocMemoryEntry
        {
            /// <summary>
            /// Offset of the data on the data file.
            /// </summary>
            public uint Offset;

            /// <summary>
            /// Code size.
            /// </summary>
            public uint CodeSize;

            /// <summary>
            /// Constant buffer 1 data size.
            /// </summary>
            public uint Cb1DataSize;

            /// <summary>
            /// Index of the shader on the cache.
            /// </summary>
            public readonly int Index;

            /// <summary>
            /// Creates a new TOC memory entry.
            /// </summary>
            /// <param name="offset">Offset of the data on the data file</param>
            /// <param name="codeSize">Code size</param>
            /// <param name="cb1DataSize">Constant buffer 1 data size</param>
            /// <param name="index">Index of the shader on the cache</param>
            public TocMemoryEntry(uint offset, uint codeSize, uint cb1DataSize, int index)
            {
                Offset = offset;
                CodeSize = codeSize;
                Cb1DataSize = cb1DataSize;
                Index = index;
            }
        }

        private Dictionary<uint, List<TocMemoryEntry>> _toc;
        private uint _tocModificationsCount;

        private (byte[], byte[])[] _cache;

        /// <summary>
        /// Creates a new disk cache guest storage.
        /// </summary>
        /// <param name="basePath">Base path of the disk shader cache</param>
        public DiskCacheGuestStorage(string basePath)
        {
            _basePath = basePath;
        }

        /// <summary>
        /// Checks if the TOC (table of contents) file for the guest cache exists.
        /// </summary>
        /// <returns>True if the file exists, false otherwise</returns>
        public bool TocFileExists()
        {
            return File.Exists(Path.Combine(_basePath, TocFileName));
        }

        /// <summary>
        /// Checks if the data file for the guest cache exists.
        /// </summary>
        /// <returns>True if the file exists, false otherwise</returns>
        public bool DataFileExists()
        {
            return File.Exists(Path.Combine(_basePath, DataFileName));
        }

        /// <summary>
        /// Opens the guest cache TOC (table of contents) file.
        /// </summary>
        /// <returns>File stream</returns>
        public Stream OpenTocFileStream()
        {
            return DiskCacheCommon.OpenFile(_basePath, TocFileName, writable: false);
        }

        /// <summary>
        /// Opens the guest cache data file.
        /// </summary>
        /// <returns>File stream</returns>
        public Stream OpenDataFileStream()
        {
            return DiskCacheCommon.OpenFile(_basePath, DataFileName, writable: false);
        }

        /// <summary>
        /// Clear all content from the guest cache files.
        /// </summary>
        public void ClearCache()
        {
            using var tocFileStream = DiskCacheCommon.OpenFile(_basePath, TocFileName, writable: true);
            using var dataFileStream = DiskCacheCommon.OpenFile(_basePath, DataFileName, writable: true);

            tocFileStream.SetLength(0);
            dataFileStream.SetLength(0);
        }

        /// <summary>
        /// Loads the guest cache from file or memory cache.
        /// </summary>
        /// <param name="tocFileStream">Guest TOC file stream</param>
        /// <param name="dataFileStream">Guest data file stream</param>
        /// <param name="index">Guest shader index</param>
        /// <returns>Guest code and constant buffer 1 data</returns>
        public GuestCodeAndCbData LoadShader(Stream tocFileStream, Stream dataFileStream, int index)
        {
            if (_cache == null || index >= _cache.Length)
            {
                _cache = new (byte[], byte[])[Math.Max(index + 1, GetShadersCountFromLength(tocFileStream.Length))];
            }

            (byte[] guestCode, byte[] cb1Data) = _cache[index];

            if (guestCode == null || cb1Data == null)
            {
                BinarySerializer tocReader = new(tocFileStream);
                tocFileStream.Seek(Unsafe.SizeOf<TocHeader>() + index * Unsafe.SizeOf<TocEntry>(), SeekOrigin.Begin);

                TocEntry entry = new();
                tocReader.Read(ref entry);

                guestCode = new byte[entry.CodeSize];
                cb1Data = new byte[entry.Cb1DataSize];

                if (entry.Offset >= (ulong)dataFileStream.Length)
                {
                    throw new DiskCacheLoadException(DiskCacheLoadResult.FileCorruptedGeneric);
                }

                dataFileStream.Seek((long)entry.Offset, SeekOrigin.Begin);
                dataFileStream.ReadExactly(cb1Data);
                BinarySerializer.ReadCompressed(dataFileStream, guestCode);

                _cache[index] = (guestCode, cb1Data);
            }

            return new GuestCodeAndCbData(guestCode, cb1Data);
        }

        /// <summary>
        /// Clears guest code memory cache, forcing future loads to be from file.
        /// </summary>
        public void ClearMemoryCache()
        {
            _cache = null;
        }

        /// <summary>
        /// Calculates the guest shaders count from the TOC file length.
        /// </summary>
        /// <param name="length">TOC file length</param>
        /// <returns>Shaders count</returns>
        private static int GetShadersCountFromLength(long length)
        {
            return (int)((length - Unsafe.SizeOf<TocHeader>()) / Unsafe.SizeOf<TocEntry>());
        }

        /// <summary>
        /// Adds a guest shader to the cache.
        /// </summary>
        /// <remarks>
        /// If the shader is already on the cache, the existing index will be returned and nothing will be written.
        /// </remarks>
        /// <param name="data">Guest code</param>
        /// <param name="cb1Data">Constant buffer 1 data accessed by the code</param>
        /// <returns>Index of the shader on the cache</returns>
        public int AddShader(ReadOnlySpan<byte> data, ReadOnlySpan<byte> cb1Data)
        {
            using var tocFileStream = DiskCacheCommon.OpenFile(_basePath, TocFileName, writable: true);
            using var dataFileStream = DiskCacheCommon.OpenFile(_basePath, DataFileName, writable: true);

            TocHeader header = new();

            LoadOrCreateToc(tocFileStream, ref header);

            uint hash = CalcHash(data, cb1Data);

            if (_toc.TryGetValue(hash, out var list))
            {
                foreach (var entry in list)
                {
                    if (data.Length != entry.CodeSize || cb1Data.Length != entry.Cb1DataSize)
                    {
                        continue;
                    }

                    dataFileStream.Seek((long)entry.Offset, SeekOrigin.Begin);
                    byte[] cachedCode = new byte[entry.CodeSize];
                    byte[] cachedCb1Data = new byte[entry.Cb1DataSize];
                    dataFileStream.ReadExactly(cachedCb1Data);
                    BinarySerializer.ReadCompressed(dataFileStream, cachedCode);

                    if (data.SequenceEqual(cachedCode) && cb1Data.SequenceEqual(cachedCb1Data))
                    {
                        return entry.Index;
                    }
                }
            }

            return WriteNewEntry(tocFileStream, dataFileStream, ref header, data, cb1Data, hash);
        }

        /// <summary>
        /// Loads the guest cache TOC file, or create a new one if not present.
        /// </summary>
        /// <param name="tocFileStream">Guest TOC file stream</param>
        /// <param name="header">Set to the TOC file header</param>
        private void LoadOrCreateToc(Stream tocFileStream, ref TocHeader header)
        {
            BinarySerializer reader = new(tocFileStream);

            if (!reader.TryRead(ref header) || header.Magic != TocMagic || header.Version != VersionPacked)
            {
                CreateToc(tocFileStream, ref header);
            }

            if (_toc == null || header.ModificationsCount != _tocModificationsCount)
            {
                if (!LoadTocEntries(tocFileStream, ref reader))
                {
                    CreateToc(tocFileStream, ref header);
                }

                _tocModificationsCount = header.ModificationsCount;
            }
        }

        /// <summary>
        /// Creates a new guest cache TOC file.
        /// </summary>
        /// <param name="tocFileStream">Guest TOC file stream</param>
        /// <param name="header">Set to the TOC header</param>
        private static void CreateToc(Stream tocFileStream, ref TocHeader header)
        {
            BinarySerializer writer = new(tocFileStream);

            header.Magic = TocMagic;
            header.Version = VersionPacked;
            header.Padding = 0;
            header.ModificationsCount = 0;
            header.Reserved = 0;
            header.Reserved2 = 0;

            if (tocFileStream.Length > 0)
            {
                tocFileStream.Seek(0, SeekOrigin.Begin);
                tocFileStream.SetLength(0);
            }

            writer.Write(ref header);
        }

        /// <summary>
        /// Reads all the entries on the guest TOC file.
        /// </summary>
        /// <param name="tocFileStream">Guest TOC file stream</param>
        /// <param name="reader">TOC file reader</param>
        /// <returns>True if the operation was successful, false otherwise</returns>
        private bool LoadTocEntries(Stream tocFileStream, ref BinarySerializer reader)
        {
            _toc = new Dictionary<uint, List<TocMemoryEntry>>();

            TocEntry entry = new();
            int index = 0;

            while (tocFileStream.Position < tocFileStream.Length)
            {
                if (!reader.TryRead(ref entry))
                {
                    return false;
                }

                AddTocMemoryEntry(entry.Offset, entry.CodeSize, entry.Cb1DataSize, entry.Hash, index++);
            }

            return true;
        }

        /// <summary>
        /// Writes a new guest code entry into the file.
        /// </summary>
        /// <param name="tocFileStream">TOC file stream</param>
        /// <param name="dataFileStream">Data file stream</param>
        /// <param name="header">TOC header, to be updated with the new count</param>
        /// <param name="data">Guest code</param>
        /// <param name="cb1Data">Constant buffer 1 data accessed by the guest code</param>
        /// <param name="hash">Code and constant buffer data hash</param>
        /// <returns>Entry index</returns>
        private int WriteNewEntry(
            Stream tocFileStream,
            Stream dataFileStream,
            ref TocHeader header,
            ReadOnlySpan<byte> data,
            ReadOnlySpan<byte> cb1Data,
            uint hash)
        {
            BinarySerializer tocWriter = new(tocFileStream);

            dataFileStream.Seek(0, SeekOrigin.End);
            uint dataOffset = checked((uint)dataFileStream.Position);
            uint codeSize = (uint)data.Length;
            uint cb1DataSize = (uint)cb1Data.Length;
            dataFileStream.Write(cb1Data);
            BinarySerializer.WriteCompressed(dataFileStream, data, DiskCacheCommon.GetCompressionAlgorithm());

            _tocModificationsCount = ++header.ModificationsCount;
            tocFileStream.Seek(0, SeekOrigin.Begin);
            tocWriter.Write(ref header);

            TocEntry entry = new()
            {
                Offset = dataOffset,
                CodeSize = codeSize,
                Cb1DataSize = cb1DataSize,
                Hash = hash,
            };

            tocFileStream.Seek(0, SeekOrigin.End);
            int index = (int)((tocFileStream.Position - Unsafe.SizeOf<TocHeader>()) / Unsafe.SizeOf<TocEntry>());

            tocWriter.Write(ref entry);

            AddTocMemoryEntry(dataOffset, codeSize, cb1DataSize, hash, index);

            return index;
        }

        /// <summary>
        /// Adds an entry to the memory TOC cache. This can be used to avoid reading the TOC file all the time.
        /// </summary>
        /// <param name="dataOffset">Offset of the code and constant buffer data in the data file</param>
        /// <param name="codeSize">Code size</param>
        /// <param name="cb1DataSize">Constant buffer 1 data size</param>
        /// <param name="hash">Code and constant buffer data hash</param>
        /// <param name="index">Index of the data on the cache</param>
        private void AddTocMemoryEntry(uint dataOffset, uint codeSize, uint cb1DataSize, uint hash, int index)
        {
            if (!_toc.TryGetValue(hash, out var list))
            {
                _toc.Add(hash, list = new List<TocMemoryEntry>());
            }

            list.Add(new TocMemoryEntry(dataOffset, codeSize, cb1DataSize, index));
        }

        /// <summary>
        /// Calculates the hash for a data pair.
        /// </summary>
        /// <param name="data">Data 1</param>
        /// <param name="data2">Data 2</param>
        /// <returns>Hash of both data</returns>
        private static uint CalcHash(ReadOnlySpan<byte> data, ReadOnlySpan<byte> data2)
        {
            return CalcHash(data2) * 23 ^ CalcHash(data);
        }

        /// <summary>
        /// Calculates the hash for data.
        /// </summary>
        /// <param name="data">Data to be hashed</param>
        /// <returns>Hash of the data</returns>
        private static uint CalcHash(ReadOnlySpan<byte> data)
        {
            return (uint)Hash128.ComputeHash(data).Low;
        }
    }
}
