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

namespace TELTest {
    using System;
    using System.Collections.Generic;
    using System.IO;
    using System.Linq;

    internal class BasicKeyValue {
        public BasicKeyValue(string directoryName) {
            DirectoryName = directoryName;
            LoadAllKeys();
        }

        public long MaximumMemoryPressure { get; set; } = 2L * 1024 * 1024 * 1024; // 2 GB
        public long CurrentMemoryPressure => m_CurrentMemoryPressure;
        public string DirectoryName { get; set; } = "Database";
        public int LastTuplesOut { get; protected set; }
        private long m_CurrentMemoryPressure = 0;
        private readonly object _dmpLock = new object();

        private readonly Dictionary<char, DateTime> m_LastAccess = new Dictionary<char, DateTime>();
        private readonly HashSet<string> m_AllKeys = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
        private readonly Dictionary<char, long> m_ChunkMemoryPressure = new Dictionary<char, long>();
        private readonly Dictionary<char, Dictionary<string, byte[]>> m_Chunks = new Dictionary<char, Dictionary<string, byte[]>>();

        public bool ContainsKey(string key) => m_AllKeys.Contains(key);

        public byte[] this[string key] {
            get {
                char q = char.ToUpper(key[0]);

                if (!m_AllKeys.Contains(key))
                    return null;

                if (!m_Chunks.ContainsKey(q) || !m_Chunks[q].ContainsKey(key)) {
                    LoadAndFreeIfNeeded(q);
                }

                if (m_Chunks.ContainsKey(q) && m_Chunks[q].ContainsKey(key)) {
                    m_LastAccess[q] = DateTime.Now;
                    return m_Chunks[q][key];
                }

                return null;
            }
            set => AddOrSet(key, value);
        }

        public void Remove(string key) {
            char c = char.ToUpper(key[0]);
            if (!m_AllKeys.Remove(key)) return;

            if (m_Chunks.ContainsKey(c)) {
                var oldValue = m_Chunks[c][key];
                m_Chunks[c].Remove(key);
                m_ChunkMemoryPressure[c] -= oldValue.Length;
                m_CurrentMemoryPressure -= oldValue.Length;
            } else {
                LoadAndFreeIfNeeded(c);
                if (m_Chunks[c].ContainsKey(key)) {
                    var loadedValue = m_Chunks[c][key];
                    if (m_Chunks[c].Remove(key)) {
                        m_ChunkMemoryPressure[c] -= loadedValue.Length;
                        m_CurrentMemoryPressure -= loadedValue.Length;
                    }
                }
            }
        }

        public void SavePendingChanges() => DumpAll();

        protected void AddOrSet(string key, byte[] value) {
            char q = char.ToUpper(key[0]);

            PrepAddChunk(q);

            if (m_Chunks[q].TryGetValue(key, out var oldValue)) {
                m_Chunks[q][key] = value;
                long delta = value.Length - oldValue.Length;
                m_ChunkMemoryPressure[q] += delta;
                m_CurrentMemoryPressure += delta;
            } else {
                m_AllKeys.Add(key);
                m_Chunks[q].Add(key, value);
                m_ChunkMemoryPressure[q] += value.Length;
                m_CurrentMemoryPressure += value.Length;
            }
        }

        protected void LoadAndFreeIfNeeded(char at) {
            while (m_CurrentMemoryPressure > MaximumMemoryPressure && m_LastAccess.Any()) {
                var oldest = m_LastAccess.OrderBy(x => x.Value).First();
                UnloadChunk(oldest.Key);
            }
            LoadChunk(at);
        }

        protected void UnloadChunk(char chk) {
            chk = char.ToUpper(chk);
            if (!m_Chunks.ContainsKey(chk)) return;

            WriteChunk(chk);

            m_CurrentMemoryPressure -= m_ChunkMemoryPressure[chk];
            m_ChunkMemoryPressure.Remove(chk);

            m_Chunks.Remove(chk);
            m_LastAccess.Remove(chk);
        }

        public void DumpAll() {
            LastTuplesOut = 0;
            lock (_dmpLock) {
                if (!Directory.Exists(DirectoryName))
                    Directory.CreateDirectory(DirectoryName);

                foreach (var item in m_Chunks.Keys.ToList()) {
                    WriteChunk(item);
                }
            }
        }

        protected void WriteChunk(char c) {
            c = char.ToUpper(c);
            if (!m_Chunks.ContainsKey(c)) return;

            string fp = Path.Combine(DirectoryName, c + ".bin");

            using (var str = File.Open(fp, FileMode.Create, FileAccess.Write))
            using (var bw = new BinaryWriter(str, System.Text.Encoding.UTF8, false)) {
                foreach (var kv in m_Chunks[c]) {
                    byte[] idBytes = System.Text.Encoding.UTF8.GetBytes(kv.Key);
                    bw.Write(idBytes.Length);
                    bw.Write(idBytes);

                    byte[] valueBytes = kv.Value ?? new byte[0];
                    bw.Write(valueBytes.Length);
                    bw.Write(valueBytes);
                    LastTuplesOut++;
                }
                bw.Write(0); // terminator
            }
        }

        protected void LoadAllKeys() {
            if (!Directory.Exists(DirectoryName)) return;

            foreach (var chunk in Directory.GetFiles(DirectoryName, "*.bin")) {
                var cn = Path.GetFileNameWithoutExtension(chunk)[0];
                m_LastAccess[cn] = DateTime.MinValue;

                using (var fs = File.OpenRead(chunk))
                using (var br = new BinaryReader(fs, System.Text.Encoding.UTF8, true)) {
                    while (true) {
                        int idlen = br.ReadInt32();
                        if (idlen == 0) break;

                        string id = System.Text.Encoding.UTF8.GetString(br.ReadBytes(idlen));
                        int ctlen = br.ReadInt32();
                        fs.Seek(ctlen, SeekOrigin.Current);

                        m_AllKeys.Add(id);
                    }
                }
            }
        }

        protected void PrepAddChunk(char chk) {
            chk = char.ToUpper(chk);
            if (!m_Chunks.ContainsKey(chk))
                m_Chunks[chk] = new Dictionary<string, byte[]>(StringComparer.OrdinalIgnoreCase);

            if (!m_ChunkMemoryPressure.ContainsKey(chk))
                m_ChunkMemoryPressure[chk] = 0L;

            m_LastAccess[chk] = DateTime.Now;
        }

        protected bool LoadChunk(char chk) {
            chk = char.ToUpper(chk);
            if (!Directory.Exists(DirectoryName)) return false;

            string path = Path.Combine(DirectoryName, chk + ".bin");
            if (!File.Exists(path)) return false;
            if (m_Chunks.ContainsKey(chk)) return true;

            PrepAddChunk(chk);

            using (var str = File.OpenRead(path))
            using (var br = new BinaryReader(str, System.Text.Encoding.UTF8, true)) {
                while (true) {
                    int idl = br.ReadInt32();
                    if (idl == 0) break;

                    string id = System.Text.Encoding.UTF8.GetString(br.ReadBytes(idl));
                    int ctlen = br.ReadInt32();
                    byte[] ctbytes = br.ReadBytes(ctlen);

                    m_Chunks[chk][id] = ctbytes;
                    m_AllKeys.Add(id);

                    long mp = id.Length + ctbytes.Length;
                    m_CurrentMemoryPressure += mp;
                    m_ChunkMemoryPressure[chk] += mp;
                }
            }

            return true;
        }
    }

}
