using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;

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

using Best.HTTP.Shared.Databases;
using Best.HTTP.Shared.Databases.Indexing;
using Best.HTTP.Shared.Databases.Indexing.Comparers;
using Best.HTTP.Shared.Databases.MetadataIndexFinders;
using Best.HTTP.Shared.Databases.Utils;

namespace Best.MQTT.Databases
{
    public sealed class TopicAliasMappingDatabaseOptions : DatabaseOptions
    {
        public TopicAliasMappingDatabaseOptions(string dbName) : base(dbName)
        {
            base.UseHashFile = false;
        }
    }

    public sealed class TopicAliasMappingMetadata : Metadata
    {
        internal UInt32 hash;
        internal UInt16 alias;
        internal bool sentToServer;

        public override void SaveTo(Stream stream)
        {
            base.SaveTo(stream);

            stream.EncodeUnsignedVariableByteInteger(this.hash);
            stream.EncodeUnsignedVariableByteInteger(this.alias);
        }

        public override void LoadFrom(Stream stream)
        {
            base.LoadFrom(stream);

            this.hash = (UInt32)stream.DecodeUnsignedVariableByteInteger();
            this.alias = (UInt16)stream.DecodeUnsignedVariableByteInteger();
        }
    }

    public sealed class TopicAliasMappingIndexingService : IndexingService<string, TopicAliasMappingMetadata>
    {
        private AVLTree<UInt32, int> index_Hash = new AVLTree<UInt32, int>(new UInt32Comparer());
        private AVLTree<UInt16, int> index_Alias = new AVLTree<UInt16, int>(new UInt16Comparer());

        public override void Index(TopicAliasMappingMetadata metadata)
        {
            base.Index(metadata);

            this.index_Hash.Add(metadata.hash, metadata.Index);
            this.index_Alias.Add(metadata.alias, metadata.Index);
        }

        public override void Remove(TopicAliasMappingMetadata metadata)
        {
            base.Remove(metadata);

            this.index_Hash.Remove(metadata.hash);
            this.index_Alias.Remove(metadata.alias);
        }

        public override void Clear()
        {
            base.Clear();

            this.index_Hash.Clear();
            this.index_Alias.Clear();
        }

        public override IEnumerable<int> GetOptimizedIndexes() => this.index_Hash.WalkHorizontal();

        public bool ContainsHash(UInt32 hash) => this.index_Hash.ContainsKey(hash);
        public bool ContainsAlias(UInt16 alias) => this.index_Alias.ContainsKey(alias);

        public List<int> FindByHash(UInt32 hash) => this.index_Hash.Find(hash);
        public List<int> FindByAlias(UInt16 alias) => this.index_Alias.Find(alias);

        public int Count() => this.index_Hash.ElemCount;
    }

    public sealed class TopicAliasMappingMetadataService : MetadataService<TopicAliasMappingMetadata, string>
    {
        public TopicAliasMappingMetadataService(IndexingService<string, TopicAliasMappingMetadata> indexingService)
            : base(indexingService, new FindDeletedMetadataIndexFinder<TopicAliasMappingMetadata>())
        {
        }

        public TopicAliasMappingMetadata Create(string hash, UInt16 alias, int filePos, int length)
        {
            var result = base.CreateDefault(hash, filePos, length, (content, metadata) => {
                metadata.hash = MurmurHash2.Hash(content);
                metadata.alias = alias;
            });

            return result;
        }
    }

    public sealed class TopicAliasMappingDiskContentParser : IDiskContentParser<string>
    {
        public void Encode(Stream stream, string content)
        {
            StreamUtil.WriteLengthPrefixedString(stream, content);
        }

        public string Parse(Stream stream, int length)
        {
            return StreamUtil.ReadLengthPrefixedString(stream);
        }
    }

    public sealed class TopicAliasMappingDatabase : Database<string, TopicAliasMappingMetadata, TopicAliasMappingIndexingService, TopicAliasMappingMetadataService>
    {
        public TopicAliasMappingDatabase(string directory, string dbName, TopicAliasMappingIndexingService indexingService)
            : base(directory, new TopicAliasMappingDatabaseOptions(dbName), indexingService, new TopicAliasMappingDiskContentParser(), new TopicAliasMappingMetadataService(indexingService))
        {
        }

        public void Add(string topicName, UInt16 topicAliasMaximum)
        {
            using (new WriteLock(this.rwlock))
            {
                // find free alias
                UInt16 alias = 0;
                for (UInt16 i = 1; i <= topicAliasMaximum; i++)
                {
                    if (!this.IndexingService.ContainsAlias(i))
                    {
                        alias = i;
                        break;
                    }
                }

                if (alias == 0)
                    throw new Exception("Couldn't found free alias!");

                Add(alias, topicName);
            }
        }

        private void Add(UInt16 alias, string topicName)
        {
            var (pos, length) = this.DiskManager.Append(topicName);
            this.MetadataService.Create(topicName, alias, pos, length);

            this.FlagDirty(1);
        }

        public void Set(UInt16 alias, string topicName)
        {
            using (new WriteLock(this.rwlock))
            {
                var metadataIndexes = this.IndexingService.FindByAlias(alias);
                if (metadataIndexes == null)
                {
                    Add(alias, topicName);
                    return;
                }

                this.Delete(metadataIndexes);

                Add(alias, topicName);
            }
        }

        public string Find(UInt16 alias)
        {
            if (alias == 0)
                return null;

            using (new ReadLock(this.rwlock))
            {
                var metadataIndexes = this.IndexingService.FindByAlias(alias);
                if (metadataIndexes == null)
                    return null;

                return this.FromMetadataIndexes(metadataIndexes).FirstOrDefault();
            }
        }

        public (UInt16 alias, bool sentToServer) Find(string topicName)
        {
            if (string.IsNullOrEmpty(topicName))
                return (0, false);

            using (new ReadLock(this.rwlock))
            {
                if (this.IndexingService.Count() == 0)
                    return (0, false);

                var hash = MurmurHash2.Hash(topicName);

                var metadataIndexes = this.IndexingService.FindByHash(hash);
                if (metadataIndexes == null)
                    return (0, false);

                var metadata = this.MetadataService.Metadatas[metadataIndexes[0]];
                return (metadata.alias, metadata.sentToServer);
            }
        }

        public UInt16 Count()
        {
            using (new ReadLock(this.rwlock))
                return (UInt16)this.IndexingService.Count();
        }

        internal void SetSent(ushort alias, bool sentToServer)
        {
            using (new WriteLock(this.rwlock))
            {
                var metadataIndexes = this.IndexingService.FindByAlias(alias);
                if (metadataIndexes == null)
                    return;

                this.MetadataService.Metadatas[metadataIndexes[0]].sentToServer = sentToServer;

                this.FlagDirty(1);
            }
        }
    }

    // https://github.com/jitbit/MurmurHash.net
    public class MurmurHash2
    {
        const uint m = 0x5bd1e995;
        const int r = 24;

        public static uint Hash(string str)
        {
            var count = System.Text.Encoding.UTF8.GetByteCount(str);
            var buffer = BufferPool.Get(count, true);

            System.Text.Encoding.UTF8.GetBytes(str, 0, str.Length, buffer, 0);

            var hash = Hash(buffer, count);

            BufferPool.Release(buffer);

            return hash;
        }

        private static uint Hash(byte[] data, int dataLength, uint seed = 0xc58f1a7a)
        {
            int length = dataLength;
            if (length == 0)
                return 0;
            uint h = seed ^ (uint)length;
            int currentIndex = 0;
            while (length >= 4)
            {
                uint k = (uint)(data[currentIndex++] | data[currentIndex++] << 8 | data[currentIndex++] << 16 | data[currentIndex++] << 24);
                k *= m;
                k ^= k >> r;
                k *= m;

                h *= m;
                h ^= k;
                length -= 4;
            }
            switch (length)
            {
                case 3:
                    h ^= (UInt16)(data[currentIndex++] | data[currentIndex++] << 8);
                    h ^= (uint)(data[currentIndex] << 16);
                    h *= m;
                    break;
                case 2:
                    h ^= (UInt16)(data[currentIndex++] | data[currentIndex] << 8);
                    h *= m;
                    break;
                case 1:
                    h ^= data[currentIndex];
                    h *= m;
                    break;
                default:
                    break;
            }

            h ^= h >> 13;
            h *= m;
            h ^= h >> 15;

            return h;
        }
    }
}
