//
// https://github.com/ServiceStack/ServiceStack.Text
// ServiceStack.Text: .NET C# POCO JSON, JSV and CSV Text Serializers.
//
// Authors:
//   Demis Bellot (demis.bellot@gmail.com)
//
// Copyright 2012 ServiceStack, Inc. All Rights Reserved.
//
// Licensed under the same terms of ServiceStack.
//

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

namespace ServiceStack.Text.Common
{
    public static class DeserializeDictionary<TSerializer>
        where TSerializer : ITypeSerializer
    {
        private static readonly ITypeSerializer Serializer = JsWriter.GetTypeSerializer<TSerializer>();

        const int KeyIndex = 0;
        const int ValueIndex = 1;

        public static ParseStringDelegate GetParseMethod(Type type) => v => GetParseStringSpanMethod(type)(v.AsSpan());

        public static ParseStringSpanDelegate GetParseStringSpanMethod(Type type)
        {
            var mapInterface = type.GetTypeWithGenericInterfaceOf(typeof(IDictionary<,>));
            if (mapInterface == null)
            {
                var fn = PclExport.Instance.GetDictionaryParseStringSpanMethod<TSerializer>(type);
                if (fn != null)
                    return fn;

                if (type == typeof(IDictionary))
                {
                    return GetParseStringSpanMethod(typeof(Dictionary<object, object>));
                }
                if (typeof(IDictionary).IsAssignableFrom(type))
                {
                    return s => ParseIDictionary(s, type);
                }

                throw new ArgumentException($"Type {type.FullName} is not of type IDictionary<,>");
            }

            //optimized access for regularly used types
            if (type == typeof(Dictionary<string, string>))
                return ParseStringDictionary;
            if (type == typeof(JsonObject))
                return ParseJsonObject;
            if (typeof(JsonObject).IsAssignableFrom(type))
            {
                var method = typeof(DeserializeDictionary<TSerializer>).GetMethod("ParseInheritedJsonObject");
                method = method.MakeGenericMethod(type);
                return Delegate.CreateDelegate(typeof(ParseStringSpanDelegate), method) as ParseStringSpanDelegate;
            }

            var dictionaryArgs = mapInterface.GetGenericArguments();
            var keyTypeParseMethod = Serializer.GetParseStringSpanFn(dictionaryArgs[KeyIndex]);
            if (keyTypeParseMethod == null) return null;

            var valueTypeParseMethod = Serializer.GetParseStringSpanFn(dictionaryArgs[ValueIndex]);
            if (valueTypeParseMethod == null) return null;

            var createMapType = type.HasAnyTypeDefinitionsOf(typeof(Dictionary<,>), typeof(IDictionary<,>))
                ? null : type;

            return value => ParseDictionaryType(value, createMapType, dictionaryArgs, keyTypeParseMethod, valueTypeParseMethod);
        }

        public static JsonObject ParseJsonObject(string value) => ParseJsonObject(value.AsSpan());
        
        public static T ParseInheritedJsonObject<T>(ReadOnlySpan<char> value) where T : JsonObject, new()
        {
            if (value.Length == 0)
                return null;

            var index = VerifyAndGetStartIndex(value, typeof(T));

            var result = new T();

            if (Json.JsonTypeSerializer.IsEmptyMap(value, index)) return result;

            var valueLength = value.Length;
            while (index < valueLength)
            {
                var keyValue = Serializer.EatMapKey(value, ref index);
                Serializer.EatMapKeySeperator(value, ref index);
                var elementValue = Serializer.EatValue(value, ref index);
                if (keyValue.IsEmpty) continue;

                var mapKey = keyValue.ToString();
                var mapValue = elementValue.Value();

                result[mapKey] = mapValue;

                Serializer.EatItemSeperatorOrMapEndChar(value, ref index);
            }

            return result;
        }

        public static JsonObject ParseJsonObject(ReadOnlySpan<char> value)
        {
            if (value.Length == 0)
                return null;

            var index = VerifyAndGetStartIndex(value, typeof(JsonObject));

            var result = new JsonObject();

            if (Json.JsonTypeSerializer.IsEmptyMap(value, index)) return result;

            var valueLength = value.Length;
            while (index < valueLength)
            {
                var keyValue = Serializer.EatMapKey(value, ref index);
                Serializer.EatMapKeySeperator(value, ref index);
                var elementValue = Serializer.EatValue(value, ref index);
                if (keyValue.IsEmpty) continue;

                var mapKey = keyValue.ToString();
                var mapValue = elementValue.Value();

                result[mapKey] = mapValue;

                Serializer.EatItemSeperatorOrMapEndChar(value, ref index);
            }

            return result;
        }

        public static Dictionary<string, string> ParseStringDictionary(string value) => ParseStringDictionary(value.AsSpan());

        public static Dictionary<string, string> ParseStringDictionary(ReadOnlySpan<char> value)
        {
            if (value.IsEmpty)
                return null;

            var index = VerifyAndGetStartIndex(value, typeof(Dictionary<string, string>));

            var result = new Dictionary<string, string>();

            if (Json.JsonTypeSerializer.IsEmptyMap(value, index)) return result;

            var valueLength = value.Length;
            while (index < valueLength)
            {
                var keyValue = Serializer.EatMapKey(value, ref index);
                Serializer.EatMapKeySeperator(value, ref index);
                var elementValue = Serializer.EatValue(value, ref index);
                if (keyValue.IsEmpty) continue;

                var mapKey = Serializer.UnescapeString(keyValue);
                var mapValue = Serializer.UnescapeString(elementValue);

                result[mapKey.ToString()] = mapValue.Value();

                Serializer.EatItemSeperatorOrMapEndChar(value, ref index);
            }

            return result;
        }

        public static IDictionary ParseIDictionary(string value, Type dictType) => ParseIDictionary(value.AsSpan(), dictType);

        public static IDictionary ParseIDictionary(ReadOnlySpan<char> value, Type dictType)
        {
            if (value.IsEmpty) return null;

            var index = VerifyAndGetStartIndex(value, dictType);

            var valueParseMethod = Serializer.GetParseStringSpanFn(typeof(object));
            if (valueParseMethod == null) return null;

            var to = (IDictionary)dictType.CreateInstance();

            if (Json.JsonTypeSerializer.IsEmptyMap(value, index)) return to;

            var valueLength = value.Length;
            while (index < valueLength)
            {
                var keyValue = Serializer.EatMapKey(value, ref index);
                Serializer.EatMapKeySeperator(value, ref index);
                var elementStartIndex = index;
                var elementValue = Serializer.EatTypeValue(value, ref index);
                if (keyValue.IsEmpty) continue;

                var mapKey = valueParseMethod(keyValue);

                if (elementStartIndex < valueLength)
                {
                    Serializer.EatWhitespace(value, ref elementStartIndex);
                    to[mapKey] = DeserializeType<TSerializer>.ParsePrimitive(elementValue.Value(), value[elementStartIndex]);
                }
                else
                {
                    to[mapKey] = valueParseMethod(elementValue);
                }

                Serializer.EatItemSeperatorOrMapEndChar(value, ref index);
            }

            return to;
        }

        public static IDictionary<TKey, TValue> ParseDictionary<TKey, TValue>(
            string value, Type createMapType,
            ParseStringDelegate parseKeyFn, ParseStringDelegate parseValueFn)
        {
            return ParseDictionary<TKey, TValue>(value.AsSpan(),
                createMapType,
                v => parseKeyFn(v.ToString()),
                v => parseValueFn(v.ToString())
                );
        }


        public static IDictionary<TKey, TValue> ParseDictionary<TKey, TValue>(
            ReadOnlySpan<char> value, Type createMapType,
            ParseStringSpanDelegate parseKeyFn, ParseStringSpanDelegate parseValueFn)
        {
            if (value.IsEmpty) return null;

            var to = (createMapType == null)
                ? new Dictionary<TKey, TValue>()
                : (IDictionary<TKey, TValue>)createMapType.CreateInstance();

            var objDeserializer = Json.JsonTypeSerializer.Instance.ObjectDeserializer;
            if (to is Dictionary<string, object> && objDeserializer != null && typeof(TSerializer) == typeof(Json.JsonTypeSerializer))
                return (IDictionary<TKey,TValue>) objDeserializer(value);

            var config = JsConfig.GetConfig();

            var tryToParseItemsAsDictionaries =
                config.ConvertObjectTypesIntoStringDictionary && typeof(TValue) == typeof(object);
            var tryToParseItemsAsPrimitiveTypes =
                config.TryToParsePrimitiveTypeValues && typeof(TValue) == typeof(object);

            var index = VerifyAndGetStartIndex(value, createMapType);

            if (Json.JsonTypeSerializer.IsEmptyMap(value, index)) return to;

            var valueLength = value.Length;
            while (index < valueLength)
            {
                var keyValue = Serializer.EatMapKey(value, ref index);
                Serializer.EatMapKeySeperator(value, ref index);
                var elementStartIndex = index;
                var elementValue = Serializer.EatTypeValue(value, ref index);
                if (keyValue.IsNullOrEmpty()) continue;

                TKey mapKey = (TKey)parseKeyFn(keyValue);

                if (tryToParseItemsAsDictionaries)
                {
                    Serializer.EatWhitespace(value, ref elementStartIndex);
                    if (elementStartIndex < valueLength && value[elementStartIndex] == JsWriter.MapStartChar)
                    {
                        var tmpMap = ParseDictionary<TKey, TValue>(elementValue, createMapType, parseKeyFn, parseValueFn);
                        if (tmpMap != null && tmpMap.Count > 0)
                        {
                            to[mapKey] = (TValue)tmpMap;
                        }
                    }
                    else if (elementStartIndex < valueLength && value[elementStartIndex] == JsWriter.ListStartChar)
                    {
                        to[mapKey] = (TValue)DeserializeList<List<object>, TSerializer>.ParseStringSpan(elementValue);
                    }
                    else
                    {
                        to[mapKey] = (TValue)(tryToParseItemsAsPrimitiveTypes && elementStartIndex < valueLength
                            ? DeserializeType<TSerializer>.ParsePrimitive(elementValue.Value(), value[elementStartIndex])
                            : parseValueFn(elementValue));
                    }
                }
                else
                {
                    if (tryToParseItemsAsPrimitiveTypes && elementStartIndex < valueLength)
                    {
                        Serializer.EatWhitespace(value, ref elementStartIndex);
                        to[mapKey] = (TValue)DeserializeType<TSerializer>.ParsePrimitive(elementValue.Value(), value[elementStartIndex]);
                    }
                    else
                    {
                        to[mapKey] = (TValue)parseValueFn(elementValue);
                    }
                }

                Serializer.EatItemSeperatorOrMapEndChar(value, ref index);
            }

            return to;
        }

        private static int VerifyAndGetStartIndex(ReadOnlySpan<char> value, Type createMapType)
        {
            var index = 0;
            if (value.Length > 0 && !Serializer.EatMapStartChar(value, ref index))
            {
                //Don't throw ex because some KeyValueDataContractDeserializer don't have '{}'
                Tracer.Instance.WriteDebug("WARN: Map definitions should start with a '{0}', expecting serialized type '{1}', got string starting with: {2}",
                    JsWriter.MapStartChar, createMapType != null ? createMapType.Name : "Dictionary<,>", value.Substring(0, value.Length < 50 ? value.Length : 50));
            }
            return index;
        }

        private static Dictionary<TypesKey, ParseDictionaryDelegate> ParseDelegateCache
            = new Dictionary<TypesKey, ParseDictionaryDelegate>();

        private delegate object ParseDictionaryDelegate(ReadOnlySpan<char> value, Type createMapType,
            ParseStringSpanDelegate keyParseFn, ParseStringSpanDelegate valueParseFn);

        public static object ParseDictionaryType(string value, Type createMapType, Type[] argTypes,
            ParseStringDelegate keyParseFn, ParseStringDelegate valueParseFn) =>
            ParseDictionaryType(value.AsSpan(), createMapType, argTypes,
                v => keyParseFn(v.ToString()), v => valueParseFn(v.ToString()));

        static readonly Type[] signature = {typeof(ReadOnlySpan<char>), typeof(Type), typeof(ParseStringSpanDelegate), typeof(ParseStringSpanDelegate)};

        public static object ParseDictionaryType(ReadOnlySpan<char> value, Type createMapType, Type[] argTypes,
            ParseStringSpanDelegate keyParseFn, ParseStringSpanDelegate valueParseFn)
        {
            var key = new TypesKey(argTypes[0], argTypes[1]);
            if (ParseDelegateCache.TryGetValue(key, out var parseDelegate))
                return parseDelegate(value, createMapType, keyParseFn, valueParseFn);

            var mi = typeof(DeserializeDictionary<TSerializer>).GetStaticMethod("ParseDictionary", signature);
            var genericMi = mi.MakeGenericMethod(argTypes);
            parseDelegate = (ParseDictionaryDelegate)genericMi.MakeDelegate(typeof(ParseDictionaryDelegate));

            Dictionary<TypesKey, ParseDictionaryDelegate> snapshot, newCache;
            do
            {
                snapshot = ParseDelegateCache;
                newCache = new Dictionary<TypesKey, ParseDictionaryDelegate>(ParseDelegateCache) {
                    [key] = parseDelegate
                };

            } while (!ReferenceEquals(
                Interlocked.CompareExchange(ref ParseDelegateCache, newCache, snapshot), snapshot));

            return parseDelegate(value, createMapType, keyParseFn, valueParseFn);
        }

        struct TypesKey
        {
            Type Type1 { get; }
            Type Type2 { get; }

            readonly int hashcode;

            public TypesKey(Type type1, Type type2)
            {
                Type1 = type1;
                Type2 = type2;
                unchecked
                {
                    hashcode = Type1.GetHashCode() ^ (37 * Type2.GetHashCode());
                }
            }

            public override bool Equals(object obj)
            {
                var types = (TypesKey)obj;

                return Type1 == types.Type1 && Type2 == types.Type2;
            }

            public override int GetHashCode() => hashcode;
        }
    }
}