using StackExchange.Redis;
using System;
using System.Collections.Generic;
using System.Linq;
using VCommon.Caching.Internals;
using VCommon.Json;

namespace VCommon.Caching
{
    public class CacheManager
    {
        private readonly RedisCacheAccess _cacheAccess;

        public CacheManager(string server) => _cacheAccess = CacheAccess.CreateRedisAccessInstance(server);

        public void KeyExpire(int db, string key, CacheExpire expire)
        {
            if (null != expire)
            {
                if (expire.To.HasValue) _cacheAccess.CacheOp(db, p => p.KeyExpire(key, expire.To.Value));
                if (expire.Specified.HasValue) _cacheAccess.CacheOp(db, p => p.KeyExpire(key, expire.Specified.Value));
            }
        }

        public bool KeyExist(int db, string key) => _cacheAccess.CacheOp(db, p => p.KeyExists(key));

        public bool KeyDelete(int db, string key) => _cacheAccess.CacheOp(db, p => p.KeyDelete(key));

        public void KeySetJson(int db, string key, object value, CacheExpire expire = null)
        {
            var json = VJsonSerializer.Serialize(value);
            _cacheAccess.CacheOp(db, p => p.StringSet(key, json));
            KeyExpire(db, key, expire);
        }

        public T KeyGetJson<T>(int db, string key) => VJsonSerializer.Deserialize<T>(_cacheAccess.CacheOp(db, p => p.StringGet(key)));

        public void KeySet(int db, string key, CacheValue value, CacheExpire expire = null)
        {
            _cacheAccess.CacheOp(db, p => p.StringSet(key, value));
            KeyExpire(db, key, expire);
        }

        public CacheValue KeyGet(int db, string key) => _cacheAccess.CacheOp(db, p => p.StringGet(key));

        public bool HashFieldExist(int db, string hashKey, string hashField) => _cacheAccess.CacheOp(db, p => p.HashExists(hashKey, hashField));

        public bool HashFieldDelete(int db, string hashKey, string hashField) => _cacheAccess.CacheOp(db, p => p.HashDelete(hashKey, hashField));

        public long HashFieldDelete(int db, string hashKey, params string[] hashFields)
        {
            if (0 == hashFields.Length) return 0;
            var fields = hashFields.Select(p => (RedisValue)p).ToArray();
            return _cacheAccess.CacheOp(db, p => p.HashDelete(hashKey, fields));
        }

        public CacheValue HashFieldFetch(int db, string hashKey, string hashField, Func<CacheValue> missingCacheValueProvider, CacheExpire expire = null)
        {
            CacheValue cv = _cacheAccess.CacheOp(db, p => p.HashGet(hashKey, hashField));
            if (false == cv.IsNull) return cv;

            var through = missingCacheValueProvider();
            if (through.IsNull) return through;

            _cacheAccess.CacheOp(db, p => p.HashSet(hashKey, hashField, through));
            KeyExpire(db, hashKey, expire);

            return through;
        }

        public T HashFieldFetchJson<T>(int db, string hashKey, string hashField, Func<T> missingCacheValueProvider, CacheExpire expire = null)
        {
            string json = _cacheAccess.CacheOp(db, p => p.HashGet(hashKey, hashField));
            if (null != json) return VJsonSerializer.Deserialize<T>(json);

            var through = missingCacheValueProvider();

            var json2 = VJsonSerializer.Serialize(through);
            _cacheAccess.CacheOp(db, p => p.HashSet(hashKey, hashField, json2));
            KeyExpire(db, hashKey, expire);

            return through;
        }

        public T HashFieldGetJson<T>(int db, string hashKey, string hashField)
        {
            var json = (string)_cacheAccess.CacheOp(db, p => p.HashGet(hashKey, hashField));
            return VJsonSerializer.Deserialize<T>(json);
        }

        public void HashFieldSetJson<T>(int db, string hashKey, string hashField, T value, CacheExpire expire = null)
        {
            var json = VJsonSerializer.Serialize(value);
            _cacheAccess.CacheOp(db, p => p.HashSet(hashKey, hashField, json));
            KeyExpire(db, hashKey, expire);
        }

        public void HashFieldSetJson<T>(int db, string hashKey, IReadOnlyDictionary<string, T> entries, CacheExpire expire = null)
        {
            var hashEntrys = entries.Select(p => new HashEntry(p.Key, VJsonSerializer.Serialize(p.Value))).ToArray();
            _cacheAccess.CacheOp(db, p => p.HashSet(hashKey, hashEntrys));
            KeyExpire(db, hashKey, expire);
        }

        public void HashFieldPurgeAndSet(int db, string hashKey, IReadOnlyDictionary<string, CacheValue> hashFields)
        {
            var entries = hashFields.Select(p => new HashEntry(p.Key, p.Value)).ToArray();
            if (0 == entries.Length) return;
            _cacheAccess.CacheOp(db, p =>
            {
                var batch = p.CreateBatch();
                batch.KeyDeleteAsync(hashKey);
                batch.HashSetAsync(hashKey, entries);
                batch.Execute();
            });
        }

        public void HashFieldDeleteByPattern(int db, string hashKey, string pattern)
        {
            var entries = _cacheAccess.CacheOp(db, p => p.HashScan(hashKey, pattern, int.MaxValue)).Select(p => p.Name).ToArray();
            _cacheAccess.CacheOp(db, p => p.HashDelete(hashKey, entries));
        }

        public IReadOnlyDictionary<string, CacheValue> HashFieldGetByPattern(int db, string hashKey, string pattern)
        {
            return _cacheAccess.CacheOp(db, p => p.HashScan(hashKey, pattern, int.MaxValue))
                .ToDictionary(p => (string)p.Name, p => (CacheValue)p.Value);
        }

        public IReadOnlyDictionary<string, T> HashFieldGetByPatternJson<T>(int db, string hashKey, string pattern)
        {
            return _cacheAccess.CacheOp(db, p => p.HashScan(hashKey, pattern, int.MaxValue))
                .ToDictionary(p => (string)p.Name, p => VJsonSerializer.Deserialize<T>(p.Value));
        }

        public void HashFieldSet(int db, string hashKey, string hashField, CacheValue value, CacheExpire expire = null)
        {
            _cacheAccess.CacheOp(db, p => p.HashSet(hashKey, hashField, value));
            KeyExpire(db, hashKey, expire);
        }

        public CacheValue HashFieldGet(int db, string hashKey, string hashField) => _cacheAccess.CacheOp(db, p => p.HashGet(hashKey, hashField));

        public void HashFieldSet(int db, string hashKey, IReadOnlyDictionary<string, CacheValue> hashField, CacheExpire expire = null)
        {
            var entries = hashField.Select(p => new HashEntry(p.Key, p.Value)).ToArray();
            if (0 == entries.Length) return;
            _cacheAccess.CacheOp(db, p => p.HashSet(hashKey, entries));
            KeyExpire(db, hashKey, expire);
        }

        public long HashFieldIncrement(int db, string hashKey, string hashField) => _cacheAccess.CacheOp(db, p => p.HashIncrement(hashKey, hashField));

        public long HashFieldDecrement(int db, string hashKey, string hashField) => _cacheAccess.CacheOp(db, p => p.HashDecrement(hashKey, hashField));

        public IReadOnlyDictionary<string, CacheValue> HashFieldGet(int db, string hashKey, params string[] hashFields)
        {
            if (0 == hashFields.Length) return new Dictionary<string, CacheValue>();

            var keys = hashFields.Distinct().ToArray();
            var fields = keys.Select(p => (RedisValue)p).ToArray();
            return _cacheAccess.CacheOp(db, p => p.HashGet(hashKey, fields))
                .Select((p, i) => new { k = keys[i], v = p })
                .ToDictionary(p => p.k, p => (CacheValue)p.v);
        }

        public IReadOnlyDictionary<TK, TV> HashFieldsFetchAsDic<TK, TV>(int db, string key, IEnumerable<TK> hashFields,
            Func<TK, string> convertHashField,
            Func<IEnumerable<TK>, IReadOnlyDictionary<TK, TV>> missingCacheValueProvider,
            Func<TV, CacheValue> convertValueToCache,
            Func<CacheValue, TV> convertCacheToValue,
            CacheExpire expire = null)
        {
            var result = new Dictionary<TK, TV>();

            var missingFields = new List<TK>(); //get from cache,and collect missing
            {
                var fields = hashFields.Distinct().ToArray();
                if (0 == fields.Length) return result;

                var hf = fields.Select(p => (RedisValue)convertHashField(p)).ToArray();

                var fromCache = _cacheAccess.CacheOp(db, p => p.HashGet(key, hf));

                for (var i = 0; i < fromCache.Length; i++)
                {
                    if (false == fromCache[i].HasValue) missingFields.Add(fields[i]);
                    else result[fields[i]] = convertCacheToValue(fromCache[i]);
                }
            }

            if (0 != missingFields.Count) //get from db,and put cache
            {
                var fromDb = missingCacheValueProvider(missingFields);
                var entries = fromDb.Select(p => new HashEntry(convertHashField(p.Key), convertValueToCache(p.Value)))
                    .Where(p => false == p.Value.IsNull)
                    .ToArray();
                _cacheAccess.CacheOp(db, p => p.HashSet(key, entries));

                //merge to result
                foreach (var kvp in fromDb) result[kvp.Key] = kvp.Value;
            }

            KeyExpire(db, key, expire);

            return result;
        }

        public string[] SearchKeys(int db, string pattern) => _cacheAccess.SearchKeys(db, pattern).Select(p => (string)p).ToArray();
    }
}
