using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using SevenZip;

namespace SevenRepacker
{
    //SOURCE: https://stackoverflow.com/a/3729877/2430943 With modify
    public class BridgeStream : Stream
    {
        private readonly long? _length;
        private readonly SevenZipExtractor _inArchive;
        private readonly int _archiveIndex;
        private bool _isInit;

        private readonly BlockingCollection<byte[]> _blocks;
        private byte[] _currentBlock;
        private int _currentBlockIndex;

        public BridgeStream(int streamWriteCountCache, long? length, SevenZipExtractor inArchive, int archiveIndex)
        {
            _length = length;
            _inArchive = inArchive;
            _archiveIndex = archiveIndex;
            _blocks = new BlockingCollection<byte[]>(streamWriteCountCache);
        }

        public override bool CanTimeout => false;
        public override bool CanRead => true;

        /// <summary>
        /// FAKE!
        /// </summary>
        public override bool CanSeek => true;

        public override bool CanWrite => true;
        public override long Length => _length ?? throw new NotSupportedException();
        public override void Flush() { }
        public long TotalBytesWritten { get; private set; }
        public int WriteCount { get; private set; }

        public override long Position
        {
            get { throw new NotSupportedException(); }
            set { throw new NotSupportedException(); }
        }

        public override long Seek(long offset, SeekOrigin origin)
        {
            throw new NotSupportedException();
        }

        public override void SetLength(long value)
        {
            throw new NotSupportedException();
        }

        public override int Read(byte[] buffer, int offset, int count)
        {
            ValidateBufferArgs(buffer, offset, count);

            if (!_isInit)
            {
                Task.Run(() =>
                {
                    _inArchive.ExtractFile(_archiveIndex, this);
                    CompleteWriting();
                });
                _isInit = true;
            }

            int bytesRead = 0;
            while (true)
            {
                if (_currentBlock != null)
                {
                    int copy = Math.Min(count - bytesRead, _currentBlock.Length - _currentBlockIndex);
                    Array.Copy(_currentBlock, _currentBlockIndex, buffer, offset + bytesRead, copy);
                    _currentBlockIndex += copy;
                    bytesRead += copy;

                    if (_currentBlock.Length <= _currentBlockIndex)
                    {
                        _currentBlock = null;
                        _currentBlockIndex = 0;
                    }

                    if (bytesRead == count)
                        return bytesRead;
                }

                if (!_blocks.TryTake(out _currentBlock, Timeout.Infinite))
                    return bytesRead;
            }
        }

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

            var newBuf = new byte[count];
            Array.Copy(buffer, offset, newBuf, 0, count);
            _blocks.Add(newBuf);
            TotalBytesWritten += count;
            WriteCount++;
        }

        protected override void Dispose(bool disposing)
        {
            base.Dispose(disposing);
            if (disposing)
            {
                _blocks.Dispose();
            }
        }

        public override void Close()
        {
            CompleteWriting();
            base.Close();
        }

        public void CompleteWriting()
        {
            _blocks.CompleteAdding();
        }

        private static void ValidateBufferArgs(byte[] buffer, int offset, int count)
        {
            if (buffer == null)
                throw new ArgumentNullException("buffer");
            if (offset < 0)
                throw new ArgumentOutOfRangeException("offset");
            if (count < 0)
                throw new ArgumentOutOfRangeException("count");
            if (buffer.Length - offset < count)
                throw new ArgumentException("buffer.Length - offset < count");
        }
    }
}