#nullable enable
using System;
using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using Swan.Reflection;

namespace Swan.Formatters {
  /// <summary>
  /// A very simple, light-weight JSON library written by Mario
  /// to teach Geo how things are done
  /// 
  /// This is an useful helper for small tasks but it doesn't represent a full-featured
  /// serializer such as the beloved Json.NET.
  /// </summary>
  public static partial class Json {
    private class Converter {
      private static readonly ConcurrentDictionary<MemberInfo, String> MemberInfoNameCache = new ConcurrentDictionary<MemberInfo, global::System.String>();

      private static readonly ConcurrentDictionary<Type, Type> ListAddMethodCache = new ConcurrentDictionary<Type, Type>();

      private readonly Object? _target;
      private readonly Type _targetType;
      private readonly Boolean _includeNonPublic;
      private readonly JsonSerializerCase _jsonSerializerCase;

      private Converter(Object? source, Type targetType, ref Object? targetInstance, Boolean includeNonPublic, JsonSerializerCase jsonSerializerCase) {
        this._targetType = targetInstance != null ? targetInstance.GetType() : targetType;
        this._includeNonPublic = includeNonPublic;
        this._jsonSerializerCase = jsonSerializerCase;

        if(source == null) {
          return;
        }

        Type sourceType = source.GetType();

        if(this._targetType == null || this._targetType == typeof(Object)) {
          this._targetType = sourceType;
        }

        if(sourceType == this._targetType) {
          this._target = source;
          return;
        }

        if(!this.TrySetInstance(targetInstance, source, ref this._target)) {
          return;
        }

        this.ResolveObject(source, ref this._target);
      }

      internal static Object? FromJsonResult(Object? source, JsonSerializerCase jsonSerializerCase, Type? targetType = null, Boolean includeNonPublic = false) {
        Object? nullRef = null;
        return new Converter(source, targetType ?? typeof(Object), ref nullRef, includeNonPublic, jsonSerializerCase).GetResult();
      }

      private static Object? FromJsonResult(Object source, Type targetType, ref Object? targetInstance, Boolean includeNonPublic) => new Converter(source, targetType, ref targetInstance, includeNonPublic, JsonSerializerCase.None).GetResult();

      private static Type? GetAddMethodParameterType(Type targetType) => ListAddMethodCache.GetOrAdd(targetType, x => x.GetMethods().FirstOrDefault(m => m.Name == AddMethodName && m.IsPublic && m.GetParameters().Length == 1)?.GetParameters()[0].ParameterType!);

      private static void GetByteArray(String sourceString, ref Object? target) {
        try {
          target = Convert.FromBase64String(sourceString);
        } // Try conversion from Base 64
        catch(FormatException) {
          target = Encoding.UTF8.GetBytes(sourceString);
        } // Get the string bytes in UTF8
      }

      private Object GetSourcePropertyValue(IDictionary<String, Object> sourceProperties, MemberInfo targetProperty) {
        String targetPropertyName = MemberInfoNameCache.GetOrAdd(targetProperty, x => AttributeCache.DefaultCache.Value.RetrieveOne<JsonPropertyAttribute>(x)?.PropertyName ?? x.Name.GetNameWithCase(this._jsonSerializerCase));

        return sourceProperties.GetValueOrDefault(targetPropertyName);
      }

      private Boolean TrySetInstance(Object? targetInstance, Object source, ref Object? target) {
        if(targetInstance == null) {
          // Try to create a default instance
          try {
            source.CreateTarget(this._targetType, this._includeNonPublic, ref target);
          } catch {
            return false;
          }
        } else {
          target = targetInstance;
        }

        return true;
      }

      private Object? GetResult() => this._target ?? this._targetType.GetDefault();

      private void ResolveObject(Object source, ref Object? target) {
        switch(source) {
          // Case 0: Special Cases Handling (Source and Target are of specific convertible types)
          // Case 0.1: Source is string, Target is byte[]
          case String sourceString when this._targetType == typeof(Byte[]):
            GetByteArray(sourceString, ref target);
            break;

          // Case 1.1: Source is Dictionary, Target is IDictionary
          case Dictionary<String, Object> sourceProperties when target is IDictionary targetDictionary:
            this.PopulateDictionary(sourceProperties, targetDictionary);
            break;

          // Case 1.2: Source is Dictionary, Target is not IDictionary (i.e. it is a complex type)
          case Dictionary<String, Object> sourceProperties:
            this.PopulateObject(sourceProperties);
            break;

          // Case 2.1: Source is List, Target is Array
          case List<Object> sourceList when target is Array targetArray:
            this.PopulateArray(sourceList, targetArray);
            break;

          // Case 2.2: Source is List,  Target is IList
          case List<Object> sourceList when target is IList targetList:
            this.PopulateIList(sourceList, targetList);
            break;

          // Case 3: Source is a simple type; Attempt conversion
          default:
            String sourceStringValue = source.ToStringInvariant();

            // Handle basic types or enumerations if not
            if(!this._targetType.TryParseBasicType(sourceStringValue, out target)) {
              this.GetEnumValue(sourceStringValue, ref target);
            }

            break;
        }
      }

      private void PopulateIList(IEnumerable<Object> objects, IList list) {
        Type? parameterType = GetAddMethodParameterType(this._targetType);
        if(parameterType == null) {
          return;
        }

        foreach(Object item in objects) {
          try {
            _ = list.Add(FromJsonResult(item, this._jsonSerializerCase, parameterType, this._includeNonPublic));
          } catch {
            // ignored
          }
        }
      }

      private void PopulateArray(IList<Object> objects, Array array) {
        Type? elementType = this._targetType.GetElementType();

        for(Int32 i = 0; i < objects.Count; i++) {
          try {
            Object? targetItem = FromJsonResult(objects[i], this._jsonSerializerCase, elementType, this._includeNonPublic);
            array.SetValue(targetItem, i);
          } catch {
            // ignored
          }
        }
      }

      private void GetEnumValue(String sourceStringValue, ref Object? target) {
        Type? enumType = Nullable.GetUnderlyingType(this._targetType);
        if(enumType == null && this._targetType.IsEnum) {
          enumType = this._targetType;
        }

        if(enumType == null) {
          return;
        }

        try {
          target = Enum.Parse(enumType, sourceStringValue);
        } catch {
          // ignored
        }
      }

      private void PopulateDictionary(IDictionary<String, Object> sourceProperties, IDictionary targetDictionary) {
        // find the add method of the target dictionary
        MethodInfo addMethod = this._targetType.GetMethods().FirstOrDefault(m => m.Name == AddMethodName && m.IsPublic && m.GetParameters().Length == 2);

        // skip if we don't have a compatible add method
        if(addMethod == null) {
          return;
        }

        global::System.Reflection.ParameterInfo[] addMethodParameters = addMethod.GetParameters();
        if(addMethodParameters[0].ParameterType != typeof(String)) {
          return;
        }

        // Retrieve the target entry type
        Type targetEntryType = addMethodParameters[1].ParameterType;

        // Add the items to the target dictionary
        foreach(KeyValuePair<String, Object> sourceProperty in sourceProperties) {
          try {
            Object? targetEntryValue = FromJsonResult(sourceProperty.Value, this._jsonSerializerCase, targetEntryType, this._includeNonPublic);
            targetDictionary.Add(sourceProperty.Key, targetEntryValue);
          } catch {
            // ignored
          }
        }
      }

      private void PopulateObject(IDictionary<String, Object> sourceProperties) {
        if(this._targetType.IsValueType) {
          this.PopulateFields(sourceProperties);
        }

        this.PopulateProperties(sourceProperties);
      }

      private void PopulateProperties(IDictionary<String, Object> sourceProperties) {
        global::System.Collections.Generic.IEnumerable<global::System.Reflection.PropertyInfo> properties = PropertyTypeCache.DefaultCache.Value.RetrieveFilteredProperties(this._targetType, false, p => p.CanWrite);

        foreach(PropertyInfo property in properties) {
          Object sourcePropertyValue = this.GetSourcePropertyValue(sourceProperties, property);
          if(sourcePropertyValue == null) {
            continue;
          }

          try {
            Object? currentPropertyValue = !property.PropertyType.IsArray ? property?.GetCacheGetMethod(this._includeNonPublic)!(this._target!) : null;

            Object? targetPropertyValue = FromJsonResult(sourcePropertyValue, property.PropertyType, ref currentPropertyValue, this._includeNonPublic);

            property?.GetCacheSetMethod(this._includeNonPublic)!(this._target!, new[] { targetPropertyValue }!);
          } catch {
            // ignored
          }
        }
      }

      private void PopulateFields(IDictionary<String, Object> sourceProperties) {
        foreach(FieldInfo field in FieldTypeCache.DefaultCache.Value.RetrieveAllFields(this._targetType)) {
          Object sourcePropertyValue = this.GetSourcePropertyValue(sourceProperties, field);
          if(sourcePropertyValue == null) {
            continue;
          }

          Object? targetPropertyValue = FromJsonResult(sourcePropertyValue, this._jsonSerializerCase, field.FieldType, this._includeNonPublic);

          try {
            field.SetValue(this._target, targetPropertyValue);
          } catch {
            // ignored
          }
        }
      }
    }
  }
}