using System;
using System.Collections.Generic;
using System.IO;
using System.Threading;

using Best.HTTP.Shared.Extensions;
using Best.HTTP.Shared.PlatformSupport.Memory;
using Best.HTTP.Shared.PlatformSupport.Threading;
using Best.HTTP.Shared.Streams;

namespace Best.HTTP.Shared.Databases
{
    public sealed class DiskManagerOptions
    {
        // avg. size of certificates is 1390 (calculated from 2621 intermediate certificate)
        public int MaxCacheSizeInBytes = 5 * 1024;

        public string HashDigest = "SHA256";
    }

    public interface IDiskContentParser<T>
    {
        T Parse(Stream stream, int length);
        void Encode(Stream stream, T content);
    }

    public sealed class DiskManager<T> : IDisposable
    {
        // TODO: store usage date/count and delete the oldest/least used?

        struct CachePointer<CacheType>
        {
            public static readonly CachePointer<CacheType> Empty = new CachePointer<CacheType> { Position = -1, Length = -1, Content = default(CacheType) };

            public int Position;
            public int Length;
            public CacheType Content;

            public override string ToString()
            {
                return $"[CachePointer<{this.Content.GetType().Name}>({Position}, {Length}, {Content})]";
            }
        }

        /// <summary>
        /// Sum size of the cached contents
        /// </summary>
        public int CacheSize { get; private set; }

        private Stream stream;

        private List<CachePointer<T>> cache = new List<CachePointer<T>>();
        private IDiskContentParser<T> diskContentParser;
        private DiskManagerOptions options;
        private FreeListManager freeListManager;

        private ReaderWriterLockSlim rwLock = new ReaderWriterLockSlim(LockRecursionPolicy.NoRecursion);

        public DiskManager(Stream stream, Stream freeListStream, IDiskContentParser<T> contentParser, DiskManagerOptions options)
        {
            if (!stream.CanSeek)
                throw new ArgumentException("DiskManager - stream can't seek!");

            this.stream = stream;
            this.freeListManager = new FreeListManager(freeListStream);
            this.diskContentParser = contentParser;
            this.options = options;
        }

        public (int, int) Append(T content)
        {
            using (new WriteLock(this.rwLock))
            {
                int pos = -1;
                int length = -1;

                using (var buffer = new BufferPoolMemoryStream())
                {
                    diskContentParser.Encode(buffer, content);

                    length = (int)buffer.Length;

                    var idx = this.freeListManager.FindFreeIndex(length);

                    if (idx >= 0)
                        pos = this.freeListManager.Occupy(idx, length);
                    else
                        pos = (int)this.stream.Length;

                    this.stream.Seek(pos, SeekOrigin.Begin);
                    buffer.Seek(0, SeekOrigin.Begin);
                    buffer.CopyTo(this.stream);
                }

                this.stream.Flush();

                return (pos, length);
            }
        }

        public void SaveChanged(Metadata metadata, T content)
        {
            using var _ = new WriteLock(this.rwLock);

            int pos = -1;
            int length = -1;

            using (var buffer = new BufferPoolMemoryStream())
            {
                diskContentParser.Encode(buffer, content);

                length = (int)buffer.Length;

                this.freeListManager.Add(metadata.FilePosition, metadata.Length);
                var idx = this.freeListManager.FindFreeIndex(length);

                if (idx >= 0)
                    pos = this.freeListManager.Occupy(idx, length);
                else
                    pos = (int)this.stream.Length;

                this.stream.Seek(pos, SeekOrigin.Begin);
                buffer.Seek(0, SeekOrigin.Begin);
                buffer.CopyTo(this.stream);
            }

            this.stream.Flush();

            metadata.FilePosition = pos;
            metadata.Length = length;
        }

        public void Delete(Metadata metadata)
        {
            using (new WriteLock(this.rwLock))
            {
                this.freeListManager.Add(metadata.FilePosition, metadata.Length);
                this.stream.Seek(metadata.FilePosition, SeekOrigin.Begin);

                var buffer = BufferPool.Get(BufferPool.MIN_BUFFER_SIZE, true);
                Array.Clear(buffer, 0, (int)BufferPool.MIN_BUFFER_SIZE);

                int length = metadata.Length;
                int iterationCount = length / (int)BufferPool.MIN_BUFFER_SIZE;
                for (int i = 0; i < iterationCount; ++i)
                {
                    this.stream.Write(buffer, 0, (int)BufferPool.MIN_BUFFER_SIZE);
                    length -= (int)BufferPool.MIN_BUFFER_SIZE;
                }
                this.stream.Write(buffer, 0, length);

                this.stream.Flush();
                BufferPool.Release(buffer);
            }
        }

        public T Load(Metadata metadata)
        {
            using (new WriteLock(this.rwLock))
            {
                T parsedContent = default(T);

                var cachePointer = GetCached(metadata.FilePosition);
                if (cachePointer.Position != -1 && cachePointer.Content != null)
                    return cachePointer.Content;

                this.stream.Seek(metadata.FilePosition, SeekOrigin.Begin);

                parsedContent = diskContentParser.Parse(this.stream, metadata.Length);

                AddToCache(parsedContent, metadata.FilePosition, metadata.Length);

                return parsedContent;
            }
        }

        public List<KeyValuePair<Meta, T>> LoadAll<Meta>(List<Meta> metadatas) where Meta : Metadata
        {
            using (new WriteLock(this.rwLock))
            {
                if (metadatas == null || metadatas.Count == 0)
                    return null;

                metadatas.Sort((m1, m2) => m1.FilePosition.CompareTo(m2.FilePosition));

                List<KeyValuePair<Meta, T>> result = new List<KeyValuePair<Meta, T>>(metadatas.Count);

                for (int i = 0; i < metadatas.Count; ++i)
                {
                    var metadata = metadatas[i];

                    result.Add(new KeyValuePair<Meta, T>(metadata, Load(metadata)));
                }

                return result;
            }
        }

        public void Clear()
        {
            using (new WriteLock(this.rwLock))
            {
                this.freeListManager.Clear();
                this.stream.SetLength(0);
                this.stream.Flush();
                this.cache.Clear();
                this.CacheSize = 0;
            }
        }

        private CachePointer<T> GetCached(int position)
        {
            for (int i = 0; i < this.cache.Count; ++i)
            {
                var cache = this.cache[i];

                if (cache.Position == position)
                    return cache;
            }

            return CachePointer<T>.Empty;
        }

        private void AddToCache(T parsedContent, int pos, int length)
        {
            if (this.options.MaxCacheSizeInBytes >= length)
            {
                this.cache.Insert(0, new CachePointer<T>
                {
                    Position = pos,
                    Length = length,
                    Content = parsedContent
                });
                this.CacheSize += length;
            }
            
            while (this.CacheSize > this.options.MaxCacheSizeInBytes && this.cache.Count > 0)
            {
                var removingCache = this.cache[this.cache.Count - 1];
                this.cache.RemoveAt(this.cache.Count - 1);

                this.CacheSize -= removingCache.Length;
            }
        }

        public BufferSegment CalculateHash()
        {
            using (new WriteLock(this.rwLock))
            {
                this.stream.Seek(0, SeekOrigin.Begin);

#if UNITY_WEBGL && !UNITY_EDITOR
                var hash = System.Security.Cryptography.HashAlgorithm.Create(this.options.HashDigest);
                var result = hash.ComputeHash(this.stream);
                return new BufferSegment(result, 0, result.Length);
#elif !BESTHTTP_DISABLE_ALTERNATE_SSL
                var digest = Best.HTTP.SecureProtocol.Org.BouncyCastle.Security.DigestUtilities.GetDigest(this.options.HashDigest);

                byte[] buffer = BufferPool.Get(4 * 1024, true);
                int readCount = 0;
                while ((readCount = this.stream.Read(buffer, 0, buffer.Length)) > 0)
                {
                    digest.BlockUpdate(buffer, 0, readCount);
                }

                BufferPool.Release(buffer);

                byte[] result = BufferPool.Get(digest.GetDigestSize(), true);

                int length = digest.DoFinal(result, 0);
                return new BufferSegment(result, 0, length);
#else
                throw new NotImplementedException(nameof(CalculateHash));
#endif
            }
        }

        public void Save()
        {
            this.stream.Flush();
            this.freeListManager.Save();
        }

        public void Dispose()
        {
            this.freeListManager.Dispose();
            this.stream.Flush();
            this.stream.Close();
            this.stream = null;
            this.rwLock.Dispose();

            GC.SuppressFinalize(this);
        }
    }
}
