// Copyright (c) 2018, Rene Lergner - wpinternals.net - @Heathcliff74xda
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the "Software"),
// to deal in the Software without restriction, including without limitation
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
// and/or sell copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.

// SevenZip LZMA SDK: http://www.7-zip.org/download.html
// Usage: http://stackoverflow.com/questions/7646328/how-to-use-the-7z-sdk-to-compress-and-decompress-a-file

using SevenZip;
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Text;
using System.Threading;

namespace WPinternals
{
    internal static class LZMA
    {
        internal static byte[] Decompress(byte[] Input, UInt32 Offset, UInt32 InputSize)
        {
            byte[] Properties = new byte[5];
            Buffer.BlockCopy(Input, (int)Offset, Properties, 0, 5);

            UInt64 OutputSize = ByteOperations.ReadUInt64(Input, Offset + 5);
            
            SevenZip.Compression.LZMA.Decoder Coder = new SevenZip.Compression.LZMA.Decoder();
            Coder.SetDecoderProperties(Properties);

            MemoryStream InStream = new MemoryStream(Input, (int)Offset + 0x0D, (int)InputSize - 0x0D);

            byte[] Output = new byte[OutputSize];
            MemoryStream OutStream = new MemoryStream(Output, true);

            Coder.Code(InStream, OutStream, (Int64)InputSize - 0x0D, (Int64)OutputSize, null);

            OutStream.Flush();
            OutStream.Close();
            InStream.Close();

            return Output;
        }

        internal static byte[] Compress(byte[] Input, UInt32 Offset, UInt32 InputSize)
        {
            SevenZip.Compression.LZMA.Encoder Coder = new SevenZip.Compression.LZMA.Encoder();

            MemoryStream InStream = new MemoryStream(Input, (int)Offset, (int)InputSize);
            MemoryStream OutStream = new MemoryStream();

            // Write the encoder properties
            Coder.WriteCoderProperties(OutStream);

            // Write the decompressed file size
            OutStream.Write(BitConverter.GetBytes(InStream.Length), 0, 8);

            // Encode the file
            Coder.Code(InStream, OutStream, (Int64)InputSize, -1, null);

            byte[] Output = new byte[OutStream.Length];
            Buffer.BlockCopy(OutStream.GetBuffer(), 0, Output, 0, (int)OutStream.Length);

            OutStream.Flush();
            OutStream.Close();
            InStream.Close();

            return Output;
        }
    }

    public class LZMACompressionStream : Stream
    {
        private SevenZip.Compression.LZMA.Encoder Encoder = null;
        private SevenZip.Compression.LZMA.Decoder Decoder = null;
        private PumpStream BufferStream;
        private Stream stream;
        private bool LeaveOpen;
        private Thread WorkThread;

        public LZMACompressionStream(Stream stream, CompressionMode mode, bool LeaveOpen, int DictionarySize, int PosStateBits,
           int LitContextBits, int LitPosBits, int Algorithm, int NumFastBytes, string MatchFinder, bool EndMarker)
        {
            this.stream = stream;
            this.LeaveOpen = LeaveOpen;
            BufferStream = new PumpStream();

            if (mode == CompressionMode.Compress)
            {
                Encoder = new SevenZip.Compression.LZMA.Encoder();
                if (DictionarySize != 0)
                    Encoder.SetCoderProperties(
                      new SevenZip.CoderPropID[8] {CoderPropID.DictionarySize, CoderPropID.PosStateBits, CoderPropID.LitContextBits,
                        CoderPropID.LitPosBits, CoderPropID.Algorithm, CoderPropID.NumFastBytes, CoderPropID.MatchFinder, CoderPropID.EndMarker},
                      new object[8] { DictionarySize, PosStateBits, LitContextBits, LitPosBits, Algorithm, NumFastBytes, MatchFinder, EndMarker });
                Encoder.WriteCoderProperties(stream);
                WorkThread = new Thread(new ThreadStart(Encode));
            }
            else
            {
                byte[] DecoderProperties = new byte[5];
                stream.Read(DecoderProperties, 0, 5);
                Decoder = new SevenZip.Compression.LZMA.Decoder();
                Decoder.SetDecoderProperties(DecoderProperties);
                WorkThread = new Thread(new ThreadStart(Decode));
            }

            WorkThread.Start();
        }

        public LZMACompressionStream(Stream stream, CompressionMode mode, bool LeaveOpen)
            : this(stream, mode, LeaveOpen, 0, 0, 0, 0, 0, 0, null, false)
        {
        }

        private void Encode()
        {
            Encoder.Code(BufferStream, stream, -1, -1, null);
            if (LeaveOpen == false)
                stream.Close();
        }

        private void Decode()
        {
            Decoder.Code(stream, BufferStream, -1, -1, null);
            BufferStream.Close();
            if (LeaveOpen == false)
                stream.Close();
        }

        public override void Close()
        {
            if (Encoder != null)
                BufferStream.Close();
            else if (WorkThread.IsAlive)
                WorkThread.Abort();
        }

        public override int Read(byte[] buffer, int offset, int count)
        {
            return BufferStream.Read(buffer, offset, count);
        }

        public override void Write(byte[] buffer, int offset, int count)
        {
            BufferStream.Write(buffer, offset, count);
        }

        public override bool CanRead { get { return (Decoder != null); } }
        public override bool CanSeek { get { return false; } }
        public override bool CanWrite { get { return (Encoder != null); } }
        public override void Flush() { }
        public override long Length { get { return 0; } }
        public override long Position { get { return 0; } set { } }
        public override long Seek(long offset, SeekOrigin origin) { return 0; }
        public override void SetLength(long value) { }
    }

    public class PumpStream : Stream
    {
        private Queue<byte[]> BufferQueue;
        private int BufferOffset;
        private long MaxBufferSize;
        private long BufferSize;
        private bool Closed;
        private bool EOF;

        public PumpStream(long MaxBufferSize, int ReadTimeout, int WriteTimeout)
        {
            this.MaxBufferSize = MaxBufferSize;
            this.ReadTimeout = ReadTimeout;
            this.WriteTimeout = WriteTimeout;
            BufferQueue = new Queue<byte[]>();
            BufferOffset = 0;
            BufferSize = 0;
            Closed = false;
            EOF = false;
        }

        public PumpStream()
            : this(16777216, Timeout.Infinite, Timeout.Infinite)
        {
        }

        public new void Dispose()
        {
            BufferQueue.Clear();
        }

        public override void Close()
        {
            Closed = true;
            lock (BufferQueue)
                Monitor.Pulse(BufferQueue);
        }

        public override int Read(byte[] buffer, int offset, int count)
        {
            int BytesRead = 0;
            lock (BufferQueue)
            {
                while (BytesRead < count && EOF == false)
                {
                    if (BufferQueue.Count > 0)
                    {
                        byte[] b = BufferQueue.Peek();

                        if ((b.Length - BufferOffset) <= (count - BytesRead))
                        {
                            Array.Copy(b, BufferOffset, buffer, offset + BytesRead, (b.Length - BufferOffset));

                            BufferQueue.Dequeue();
                            BufferSize -= b.Length;
                            Monitor.Pulse(BufferQueue);

                            BytesRead += (b.Length - BufferOffset);
                            BufferOffset = 0;
                        }
                        else
                        {
                            Array.Copy(b, BufferOffset, buffer, offset + BytesRead, (count - BytesRead));

                            BufferOffset += (count - BytesRead);
                            BytesRead += (count - BytesRead);
                        }

                    }
                    else
                    {
                        if (Closed == false)
                        {
                            if (Monitor.Wait(BufferQueue, ReadTimeout) == false)
                                throw new IOException("Could not read from stream: Timeout expired waiting for data to be written.");
                        }
                        else
                            EOF = true;
                    }
                }
            }

            return BytesRead;
        }

        public override void Write(byte[] buffer, int offset, int count)
        {
            lock (BufferQueue)
            {
                while (BufferSize >= MaxBufferSize)
                    if (Monitor.Wait(BufferQueue, WriteTimeout) == false)
                        throw new IOException("Could not write to stream: Timeout expired waiting for data to be read.");

                byte[] b = new byte[count];
                Array.Copy(buffer, offset, b, 0, count);
                BufferQueue.Enqueue(b);
                BufferSize += b.Length;

                Monitor.Pulse(BufferQueue);
            }
        }

        public override int ReadTimeout { get; set; }
        public override int WriteTimeout { get; set; }
        public override bool CanRead { get { return true; } }
        public override bool CanSeek { get { return false; } }
        public override bool CanWrite { get { return true; } }
        public override void Flush() { }
        public override long Length { get { return 0; } }
        public override long Position { get { return 0; } set { } }
        public override long Seek(long offset, SeekOrigin origin) { return 0; }
        public override void SetLength(long value) { }
    }
}
