using ICSharpCode.SharpZipLib.Zip.Compression.Streams;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using static OpenDiablo2.Common.Models.MPQ;


namespace OpenDiablo2.Common.Models
{
    public sealed class MPQStream : Stream
    {
        private readonly MPQ mpq;
        private readonly BlockRecord blockRecord;
        private uint[] blockPositions;
        private long position;
        private byte[] _currentData;
        private int _currentBlockIndex = -1;
        private int blockSize;

        internal MPQStream(MPQ mpq, BlockRecord blockRecord)
        {
            this.mpq = mpq;
            this.blockRecord = blockRecord;
            this.blockSize = 0x200 << mpq.Header.BlockSize;

            if (blockRecord.IsCompressed && !blockRecord.SingleUnit)
                LoadBlockOffsets();

        }

        private void LoadBlockOffsets()
        {

            int blockposcount = (int)((blockRecord.FileSize + blockSize - 1) / blockSize) + 1;
            blockPositions = new uint[blockposcount];

            lock (mpq.fileStream)
            {
                mpq.fileStream.Seek(blockRecord.BlockOffset, SeekOrigin.Begin);
                var br = new BinaryReader(mpq.fileStream);
                for (int i = 0; i < blockposcount; i++)
                    blockPositions[i] = br.ReadUInt32();
                
            }

            uint blockpossize = (uint)blockposcount * 4;
            
            if (blockRecord.IsEncrypted)
            {
                MPQ.DecryptBlock(blockPositions, blockRecord.EncryptionSeed - 1);

                if (blockPositions[0] != blockpossize)
                    throw new ApplicationException("Decryption failed");
                if (blockPositions[1] > blockSize + blockpossize)
                    throw new ApplicationException("Decryption failed");
            }

        }

        public override bool CanRead => true;

        public override bool CanSeek => true;

        public override bool CanWrite => false;

        public override long Length => blockRecord.FileSize;

        public override long Position
        {
            get => position;
            set => Seek(value, SeekOrigin.Begin);
        }

        public override void Flush() { }

        public override int Read(byte[] buffer, int offset, int count)
        {
            if (blockRecord.SingleUnit)
                return ReadInternalSingleUnit(buffer, offset, count);

            int toread = count;
            int readtotal = 0;

            while (toread > 0)
            {
                int read = ReadInternal(buffer, offset, toread);
                if (read == 0) break;
                readtotal += read;
                offset += read;
                toread -= read;
            }
            return readtotal;
        }

        private int ReadInternalSingleUnit(byte[] buffer, int offset, int count)
        {
            if (position >= Length)
                return 0;

            if (_currentData == null)
                LoadSingleUnit();

            int bytestocopy = Math.Min((int)(_currentData.Length - position), count);

            Array.Copy(_currentData, position, buffer, offset, bytestocopy);

            position += bytestocopy;
            return bytestocopy;
        }

        private void LoadSingleUnit()
        {
            // Read the entire file into memory
            byte[] filedata = new byte[blockSize];
            lock (mpq.fileStream)
            {
                mpq.fileStream.Seek(mpq.Header.HeaderSize + blockRecord.BlockOffset, SeekOrigin.Begin);
                int read = mpq.fileStream.Read(filedata, 0, filedata.Length);
                if (read != filedata.Length)
                    throw new ApplicationException("Insufficient data or invalid data length");
            }

            if (blockSize == blockRecord.FileSize)
                _currentData = filedata;
            else
                _currentData = DecompressMulti(filedata, (int)blockRecord.FileSize);
        }

        private int ReadInternal(byte[] buffer, int offset, int count)
        {
            // OW: avoid reading past the contents of the file
            if (position >= Length)
                return 0;

            BufferData();

            int localposition = (int)(position % blockSize);
            int bytestocopy = Math.Min(_currentData.Length - localposition, count);
            if (bytestocopy <= 0) return 0;

            Array.Copy(_currentData, localposition, buffer, offset, bytestocopy);

            position += bytestocopy;
            return bytestocopy;
        }

        public override int ReadByte()
        {
            if (position >= Length) return -1;

            if (blockRecord.SingleUnit)
                return ReadByteSingleUnit();

            BufferData();

            int localposition = (int)(position % blockSize);
            position++;
            return _currentData[localposition];
        }

        private int ReadByteSingleUnit()
        {
            if (_currentData == null)
                LoadSingleUnit();

            return _currentData[position++];
        }

        private void BufferData()
        {
            int requiredblock = (int)(position / blockSize);
            if (requiredblock != _currentBlockIndex)
            {
                int expectedlength = (int)Math.Min(Length - (requiredblock * blockSize), blockSize);
                _currentData = LoadBlock(requiredblock, expectedlength);
                _currentBlockIndex = requiredblock;
            }
        }

        private byte[] LoadBlock(int blockIndex, int expectedLength)
        {
            uint offset;
            int toread;
            uint encryptionseed;

            if (blockRecord.IsCompressed)
            {
                offset = blockPositions[blockIndex];
                toread = (int)(blockPositions[blockIndex + 1] - offset);
            }
            else
            {
                offset = (uint)(blockIndex * blockSize);
                toread = expectedLength;
            }
            offset += blockRecord.BlockOffset;

            byte[] data = new byte[toread];
            lock (mpq.fileStream)
            {
                mpq.fileStream.Seek(offset, SeekOrigin.Begin);
                int read = mpq.fileStream.Read(data, 0, toread);
                if (read != toread)
                    throw new ApplicationException("Insufficient data or invalid data length");
            }

            if (blockRecord.IsEncrypted && blockRecord.FileSize > 3)
            {
                if (blockRecord.EncryptionSeed == 0)
                    throw new ApplicationException("Unable to determine encryption key");

                encryptionseed = (uint)(blockIndex + blockRecord.EncryptionSeed);
                MPQ.DecryptBlock(data, encryptionseed);
            }

            if (blockRecord.IsCompressed && (toread != expectedLength))
            {
                //if ((blockRecord.Flags & MpqFileFlags.CompressedMulti) != 0)
                if (!blockRecord.SingleUnit)
                    data = DecompressMulti(data, expectedLength);
                else
                    data = PKDecompress(new MemoryStream(data), expectedLength);
            }

            return data;
        }

        private static byte[] DecompressMulti(byte[] input, int outputLength)
        {
            Stream sinput = new MemoryStream(input);

            byte comptype = (byte)sinput.ReadByte();

            switch (comptype)
            {
                case 1: // Huffman
                    return MpqHuffman.Decompress(sinput).ToArray();
                case 2: // ZLib/Deflate
                    return ZlibDecompress(sinput, outputLength);
                case 8: // PKLib/Impode
                    return PKDecompress(sinput, outputLength);
                case 0x10: // BZip2
                    return BZip2Decompress(sinput, outputLength);
                case 0x80: // IMA ADPCM Stereo
                    return MpqWavCompression.Decompress(sinput, 2);
                case 0x40: // IMA ADPCM Mono
                    return MpqWavCompression.Decompress(sinput, 1);

                case 0x12:
                    throw new ApplicationException("LZMA compression is not yet supported");
                // Combos
                case 0x22:
                    // TODO: sparse then zlib
                    throw new ApplicationException("Sparse compression + Deflate compression is not yet supported");
                case 0x30:
                    // TODO: sparse then bzip2
                    throw new ApplicationException("Sparse compression + BZip2 compression is not yet supported");
                case 0x41:
                    sinput = MpqHuffman.Decompress(sinput);
                    return MpqWavCompression.Decompress(sinput, 1);
                case 0x48:
                    {
                        byte[] result = PKDecompress(sinput, outputLength);
                        return MpqWavCompression.Decompress(new MemoryStream(result), 1);
                    }
                case 0x81:
                    sinput = MpqHuffman.Decompress(sinput);
                    return MpqWavCompression.Decompress(sinput, 2);
                case 0x88:
                    {
                        byte[] result = PKDecompress(sinput, outputLength);
                        return MpqWavCompression.Decompress(new MemoryStream(result), 2);
                    }
                default:
                    throw new ApplicationException("Compression is not yet supported: 0x" + comptype.ToString("X"));
            }
        }

        private static byte[] BZip2Decompress(Stream data, int expectedLength)
        {
            using (var output = new MemoryStream(expectedLength))
            {
                new Ionic.BZip2.BZip2InputStream(data)
                    .CopyTo(output);
                return output.ToArray();
            }
        }

        private static byte[] PKDecompress(Stream data, int expectedLength)
        {
            PKLibDecompress pk = new PKLibDecompress(data);
            return pk.Explode(expectedLength);
        }

        private static byte[] ZlibDecompress(Stream data, int expectedLength)
        {
            // This assumes that Zlib won't be used in combination with another compression type
            byte[] Output = new byte[expectedLength];
            Stream s = new InflaterInputStream(data);
            int Offset = 0;
            while (expectedLength > 0)
            {
                int size = s.Read(Output, Offset, expectedLength);
                if (size == 0) break;
                Offset += size;
                expectedLength -= size;
            }
            return Output;
        }

        public override long Seek(long offset, SeekOrigin origin)
        {
            long target;

            switch (origin)
            {
                case SeekOrigin.Begin:
                    target = offset;
                    break;
                case SeekOrigin.Current:
                    target = Position + offset;
                    break;
                case SeekOrigin.End:
                    target = Length + offset;
                    break;
                default:
                    throw new ArgumentException("Origin", "Invalid SeekOrigin");
            }

            if (target < 0)
                throw new ArgumentOutOfRangeException("Attmpted to Seek before the beginning of the stream");
            if (target >= Length)
                throw new ArgumentOutOfRangeException("Attmpted to Seek beyond the end of the stream");

            position = target;

            return position;
        }

        internal static uint DetectFileSeed(uint value0, uint value1, uint decrypted)
        {
            uint temp = (value0 ^ decrypted) - 0xeeeeeeee;

            for (int i = 0; i < 0x100; i++)
            {
                uint seed1 = temp - MPQ.cryptTable[0x400 + i];
                uint seed2 = 0xeeeeeeee + MPQ.cryptTable[0x400 + (seed1 & 0xff)];
                uint result = value0 ^ (seed1 + seed2);

                if (result != decrypted)
                    continue;

                uint saveseed1 = seed1;

                // Test this result against the 2nd value
                seed1 = ((~seed1 << 21) + 0x11111111) | (seed1 >> 11);
                seed2 = result + seed2 + (seed2 << 5) + 3;

                seed2 += MPQ.cryptTable[0x400 + (seed1 & 0xff)];
                result = value1 ^ (seed1 + seed2);

                if ((result & 0xfffc0000) == 0)
                    return saveseed1;
            }
            return 0;
        }

        public override void SetLength(long value) => throw new NotImplementedException();
        public override void Write(byte[] buffer, int offset, int count) => throw new NotImplementedException();
    }
}
