using Ryujinx.Horizon.Common;
using Ryujinx.Horizon.Sdk.Fs;
using System;
using System.IO;
using System.IO.Compression;

namespace Ryujinx.Horizon.Sdk.Ngc.Detail
{
    class ContentsReader : IDisposable
    {
        private const string MountName = "NgWord";
        private const string VersionFilePath = $"{MountName}:/version.dat";
        private const ulong DataId = 0x100000000000823UL;

        private enum AcType
        {
            AcNotB,
            AcB1,
            AcB2,
            AcSimilarForm,
            TableSimilarForm,
        }

        private readonly IFsClient _fsClient;
        private readonly object _lock;
        private bool _intialized;
        private ulong _cacheSize;

        public ContentsReader(IFsClient fsClient)
        {
            _lock = new();
            _fsClient = fsClient;
        }

        private static void MakeMountPoint(out string path, AcType type, int regionIndex)
        {
            path = null;

            switch (type)
            {
                case AcType.AcNotB:
                    if (regionIndex < 0)
                    {
                        path = $"{MountName}:/ac_common_not_b_nx";
                    }
                    else
                    {
                        path = $"{MountName}:/ac_{regionIndex}_not_b_nx";
                    }
                    break;
                case AcType.AcB1:
                    if (regionIndex < 0)
                    {
                        path = $"{MountName}:/ac_common_b1_nx";
                    }
                    else
                    {
                        path = $"{MountName}:/ac_{regionIndex}_b1_nx";
                    }
                    break;
                case AcType.AcB2:
                    if (regionIndex < 0)
                    {
                        path = $"{MountName}:/ac_common_b2_nx";
                    }
                    else
                    {
                        path = $"{MountName}:/ac_{regionIndex}_b2_nx";
                    }
                    break;
                case AcType.AcSimilarForm:
                    path = $"{MountName}:/ac_similar_form_nx";
                    break;
                case AcType.TableSimilarForm:
                    path = $"{MountName}:/table_similar_form_nx";
                    break;
            }
        }

        public Result Initialize(ulong cacheSize)
        {
            lock (_lock)
            {
                if (_intialized)
                {
                    return Result.Success;
                }

                Result result = _fsClient.QueryMountSystemDataCacheSize(out long dataCacheSize, DataId);
                if (result.IsFailure)
                {
                    return result;
                }

                if (cacheSize < (ulong)dataCacheSize)
                {
                    return NgcResult.InvalidSize;
                }

                result = _fsClient.MountSystemData(MountName, DataId);
                if (result.IsFailure)
                {
                    // Official firmware would return the result here,
                    // we don't to support older firmware where the archive didn't exist yet.
                    return Result.Success;
                }

                _cacheSize = cacheSize;
                _intialized = true;

                return Result.Success;
            }
        }

        public Result Reload()
        {
            lock (_lock)
            {
                if (!_intialized)
                {
                    return Result.Success;
                }

                _fsClient.Unmount(MountName);

                Result result = Result.Success;

                try
                {
                    result = _fsClient.QueryMountSystemDataCacheSize(out long cacheSize, DataId);
                    if (result.IsFailure)
                    {
                        return result;
                    }

                    if (_cacheSize < (ulong)cacheSize)
                    {
                        result = NgcResult.InvalidSize;
                        return NgcResult.InvalidSize;
                    }

                    result = _fsClient.MountSystemData(MountName, DataId);
                    if (result.IsFailure)
                    {
                        return result;
                    }
                }
                finally
                {
                    if (result.IsFailure)
                    {
                        _intialized = false;
                        _cacheSize = 0;
                    }
                }
            }

            return Result.Success;
        }

        private Result GetFileSize(out long size, string filePath)
        {
            size = 0;

            lock (_lock)
            {
                Result result = _fsClient.OpenFile(out FileHandle handle, filePath, OpenMode.Read);
                if (result.IsFailure)
                {
                    return result;
                }

                try
                {
                    result = _fsClient.GetFileSize(out size, handle);
                    if (result.IsFailure)
                    {
                        return result;
                    }
                }
                finally
                {
                    _fsClient.CloseFile(handle);
                }
            }

            return Result.Success;
        }

        private Result GetFileContent(Span<byte> destination, string filePath)
        {
            lock (_lock)
            {
                Result result = _fsClient.OpenFile(out FileHandle handle, filePath, OpenMode.Read);
                if (result.IsFailure)
                {
                    return result;
                }

                try
                {
                    result = _fsClient.ReadFile(handle, 0, destination);
                    if (result.IsFailure)
                    {
                        return result;
                    }
                }
                finally
                {
                    _fsClient.CloseFile(handle);
                }
            }

            return Result.Success;
        }

        public Result GetVersionDataSize(out long size)
        {
            return GetFileSize(out size, VersionFilePath);
        }

        public Result GetVersionData(Span<byte> destination)
        {
            return GetFileContent(destination, VersionFilePath);
        }

        public Result ReadDictionaries(out AhoCorasick partialWordsTrie, out AhoCorasick completeWordsTrie, out AhoCorasick delimitedWordsTrie, int regionIndex)
        {
            completeWordsTrie = null;
            delimitedWordsTrie = null;

            MakeMountPoint(out string partialWordsTriePath, AcType.AcNotB, regionIndex);
            MakeMountPoint(out string completeWordsTriePath, AcType.AcB1, regionIndex);
            MakeMountPoint(out string delimitedWordsTriePath, AcType.AcB2, regionIndex);

            Result result = ReadDictionary(out partialWordsTrie, partialWordsTriePath);
            if (result.IsFailure)
            {
                return NgcResult.DataAccessError;
            }

            result = ReadDictionary(out completeWordsTrie, completeWordsTriePath);
            if (result.IsFailure)
            {
                return NgcResult.DataAccessError;
            }

            return ReadDictionary(out delimitedWordsTrie, delimitedWordsTriePath);
        }

        public Result ReadSimilarFormDictionary(out AhoCorasick similarFormTrie)
        {
            MakeMountPoint(out string similarFormTriePath, AcType.AcSimilarForm, 0);

            return ReadDictionary(out similarFormTrie, similarFormTriePath);
        }

        public Result ReadSimilarFormTable(out SimilarFormTable similarFormTable)
        {
            similarFormTable = null;

            MakeMountPoint(out string similarFormTablePath, AcType.TableSimilarForm, 0);

            Result result = ReadGZipCompressedArchive(out byte[] data, similarFormTablePath);
            if (result.IsFailure)
            {
                return result;
            }

            BinaryReader reader = new(data);
            SimilarFormTable table = new();

            if (!table.Import(ref reader))
            {
                // Official firmware doesn't return an error here and just assumes the import was successful.
                return NgcResult.DataAccessError;
            }

            similarFormTable = table;

            return Result.Success;
        }

        public static Result ReadNotSeparatorDictionary(out AhoCorasick notSeparatorTrie)
        {
            notSeparatorTrie = null;

            BinaryReader reader = new(EmbeddedTries.NotSeparatorTrie);
            AhoCorasick ac = new();

            if (!ac.Import(ref reader))
            {
                // Official firmware doesn't return an error here and just assumes the import was successful.
                return NgcResult.DataAccessError;
            }

            notSeparatorTrie = ac;

            return Result.Success;
        }

        private Result ReadDictionary(out AhoCorasick trie, string path)
        {
            trie = null;

            Result result = ReadGZipCompressedArchive(out byte[] data, path);
            if (result.IsFailure)
            {
                return result;
            }

            BinaryReader reader = new(data);
            AhoCorasick ac = new();

            if (!ac.Import(ref reader))
            {
                // Official firmware doesn't return an error here and just assumes the import was successful.
                return NgcResult.DataAccessError;
            }

            trie = ac;

            return Result.Success;
        }

        private Result ReadGZipCompressedArchive(out byte[] data, string filePath)
        {
            data = null;

            Result result = _fsClient.OpenFile(out FileHandle handle, filePath, OpenMode.Read);
            if (result.IsFailure)
            {
                return result;
            }

            try
            {
                result = _fsClient.GetFileSize(out long fileSize, handle);
                if (result.IsFailure)
                {
                    return result;
                }

                data = new byte[fileSize];

                result = _fsClient.ReadFile(handle, 0, data.AsSpan());
                if (result.IsFailure)
                {
                    return result;
                }
            }
            finally
            {
                _fsClient.CloseFile(handle);
            }

            try
            {
                data = DecompressGZipCompressedStream(data);
            }
            catch (InvalidDataException)
            {
                // Official firmware returns a different error, but it is translated to this error on the caller.
                return NgcResult.DataAccessError;
            }

            return Result.Success;
        }

        private static byte[] DecompressGZipCompressedStream(byte[] data)
        {
            using MemoryStream input = new(data);
            using GZipStream gZipStream = new(input, CompressionMode.Decompress);
            using MemoryStream output = new();

            gZipStream.CopyTo(output);

            return output.ToArray();
        }

        protected virtual void Dispose(bool disposing)
        {
            if (disposing)
            {
                lock (_lock)
                {
                    if (!_intialized)
                    {
                        return;
                    }

                    _fsClient.Unmount(MountName);
                    _intialized = false;
                }
            }
        }

        public void Dispose()
        {
            Dispose(disposing: true);
            GC.SuppressFinalize(this);
        }
    }
}