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

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 partial class Json {
    /// <summary>
    /// A simple JSON serializer.
    /// </summary>
    private class Serializer {
      #region Private Declarations

      private static readonly Dictionary<Int32, String> IndentStrings = new Dictionary<global::System.Int32, global::System.String>();

      private readonly SerializerOptions? _options;
      private readonly String _result;
      private readonly StringBuilder? _builder;
      private readonly String? _lastCommaSearch;

      #endregion

      #region Constructors

      /// <summary>
      /// Initializes a new instance of the <see cref="Serializer" /> class.
      /// </summary>
      /// <param name="obj">The object.</param>
      /// <param name="depth">The depth.</param>
      /// <param name="options">The options.</param>
      private Serializer(Object? obj, Int32 depth, SerializerOptions options) {
        if(depth > 20) {
          throw new InvalidOperationException("The max depth (20) has been reached. Serializer can not continue.");
        }

        // Basic Type Handling (nulls, strings, number, date and bool)
        this._result = ResolveBasicType(obj);

        if(!String.IsNullOrWhiteSpace(this._result)) {
          return;
        }

        this._options = options;

        // Handle circular references correctly and avoid them
        if(options.IsObjectPresent(obj!)) {
          this._result = $"{{ \"$circref\": \"{Escape(obj!.GetHashCode().ToStringInvariant(), false)}\" }}";
          return;
        }

        // At this point, we will need to construct the object with a StringBuilder.
        this._lastCommaSearch = FieldSeparatorChar + (this._options.Format ? Environment.NewLine : String.Empty);
        this._builder = new StringBuilder();

        this._result = obj switch
        {
          IDictionary itemsZero when itemsZero.Count == 0 => EmptyObjectLiteral,
          IDictionary items => this.ResolveDictionary(items, depth),
          IEnumerable enumerableZero when !enumerableZero.Cast<Object>().Any() => EmptyArrayLiteral,
          IEnumerable enumerableBytes when enumerableBytes is Byte[] bytes => Serialize(bytes.ToBase64(), depth, this._options),
          IEnumerable enumerable => this.ResolveEnumerable(enumerable, depth),
          _ => this.ResolveObject(obj!, depth)
        };
      }

      internal static String Serialize(Object? obj, Int32 depth, SerializerOptions options) => new Serializer(obj, depth, options)._result;

      #endregion

      #region Helper Methods

      private static String ResolveBasicType(Object? obj) {
        switch(obj) {
          case null:
            return NullLiteral;
          case String s:
            return Escape(s, true);
          case Boolean b:
            return b ? TrueLiteral : FalseLiteral;
          case Type _:
          case Assembly _:
          case MethodInfo _:
          case PropertyInfo _:
          case EventInfo _:
            return Escape(obj.ToString()!, true);
          case DateTime d:
            return $"{StringQuotedChar}{d:s}{StringQuotedChar}";
          default:
            Type targetType = obj.GetType();

            if(!Definitions.BasicTypesInfo.Value.ContainsKey(targetType)) {
              return String.Empty;
            }

            String escapedValue = Escape(Definitions.BasicTypesInfo.Value[targetType].ToStringInvariant(obj), false);

            return Decimal.TryParse(escapedValue, out _) ? $"{escapedValue}" : $"{StringQuotedChar}{escapedValue}{StringQuotedChar}";
        }
      }

      private static Boolean IsNonEmptyJsonArrayOrObject(String serialized) {
        if(serialized == EmptyObjectLiteral || serialized == EmptyArrayLiteral) {
          return false;
        }

        // find the first position the character is not a space
        return serialized.Where(c => c != ' ').Select(c => c == OpenObjectChar || c == OpenArrayChar).FirstOrDefault();
      }

      private static String Escape(String str, Boolean quoted) {
        if(str == null) {
          return String.Empty;
        }

        StringBuilder builder = new StringBuilder(str.Length * 2);
        if(quoted) {
          _ = builder.Append(StringQuotedChar);
        }

        Escape(str, builder);
        if(quoted) {
          _ = builder.Append(StringQuotedChar);
        }

        return builder.ToString();
      }

      private static void Escape(String str, StringBuilder builder) {
        foreach(Char currentChar in str) {
          switch(currentChar) {
            case '\\':
            case '"':
            case '/':
              _ = builder
                  .Append('\\')
                  .Append(currentChar);
              break;
            case '\b':
              _ = builder.Append("\\b");
              break;
            case '\t':
              _ = builder.Append("\\t");
              break;
            case '\n':
              _ = builder.Append("\\n");
              break;
            case '\f':
              _ = builder.Append("\\f");
              break;
            case '\r':
              _ = builder.Append("\\r");
              break;
            default:
              if(currentChar < ' ') {
                Byte[] escapeBytes = BitConverter.GetBytes((UInt16)currentChar);
                if(BitConverter.IsLittleEndian == false) {
                  Array.Reverse(escapeBytes);
                }

                _ = builder.Append("\\u")
                                    .Append(escapeBytes[1].ToString("X").PadLeft(2, '0'))
                                    .Append(escapeBytes[0].ToString("X").PadLeft(2, '0'));
              } else {
                _ = builder.Append(currentChar);
              }

              break;
          }
        }
      }

      private Dictionary<String, Object?> CreateDictionary(Dictionary<String, MemberInfo> fields, String targetType, Object target) {
        // Create the dictionary and extract the properties
        global::System.Collections.Generic.Dictionary<global::System.String, global::System.Object?> objectDictionary = new Dictionary<global::System.String, global::System.Object?>();

        if(String.IsNullOrWhiteSpace(this._options?.TypeSpecifier) == false) {
          objectDictionary[this._options?.TypeSpecifier!] = targetType;
        }

        foreach(global::System.Collections.Generic.KeyValuePair<global::System.String, global::System.Reflection.MemberInfo> field in fields) {
          // Build the dictionary using property names and values
          // Note: used to be: property.GetValue(target); but we would be reading private properties
          try {
            objectDictionary[field.Key] = field.Value is PropertyInfo property ? property.GetCacheGetMethod((Boolean)(this._options?.IncludeNonPublic)!)?.Invoke(target) : (field.Value as FieldInfo)?.GetValue(target);
          } catch {
            /* ignored */
          }
        }

        return objectDictionary;
      }

      private String ResolveDictionary(IDictionary items, Int32 depth) {
        this.Append(OpenObjectChar, depth);
        this.AppendLine();

        // Iterate through the elements and output recursively
        Int32 writeCount = 0;
        foreach(Object? key in items.Keys) {
          // Serialize and append the key (first char indented)
          this.Append(StringQuotedChar, depth + 1);
          Escape(key?.ToString()!, this._builder!);
          _ = this._builder?.Append(StringQuotedChar).Append(ValueSeparatorChar).Append(" ");

          // Serialize and append the value
          String serializedValue = Serialize(items[key!], depth + 1, this._options!);

          if(IsNonEmptyJsonArrayOrObject(serializedValue)) {
            this.AppendLine();
          }

          this.Append(serializedValue, 0);

          // Add a comma and start a new line -- We will remove the last one when we are done writing the elements
          this.Append(FieldSeparatorChar, 0);
          this.AppendLine();
          writeCount++;
        }

        // Output the end of the object and set the result
        this.RemoveLastComma();
        this.Append(CloseObjectChar, writeCount > 0 ? depth : 0);
        return this._builder!.ToString();
      }

      private String ResolveObject(Object target, Int32 depth) {
        Type targetType = target.GetType();

        if(targetType.IsEnum) {
          return Convert.ToInt64(target, System.Globalization.CultureInfo.InvariantCulture).ToString();
        }

        global::System.Collections.Generic.Dictionary<global::System.String, global::System.Reflection.MemberInfo> fields = this._options!.GetProperties(targetType);

        if(fields.Count == 0 && String.IsNullOrWhiteSpace(this._options.TypeSpecifier)) {
          return EmptyObjectLiteral;
        }

        // If we arrive here, then we convert the object into a 
        // dictionary of property names and values and call the serialization
        // function again
        global::System.Collections.Generic.Dictionary<global::System.String, global::System.Object?> objectDictionary = this.CreateDictionary(fields, targetType.ToString(), target);

        return Serialize(objectDictionary, depth, this._options);
      }

      private String ResolveEnumerable(IEnumerable target, Int32 depth) {
        // Cast the items as a generic object array
        global::System.Collections.Generic.IEnumerable<global::System.Object> items = target.Cast<global::System.Object>();

        this.Append(OpenArrayChar, depth);
        this.AppendLine();

        // Iterate through the elements and output recursively
        Int32 writeCount = 0;
        foreach(Object entry in items) {
          String serializedValue = Serialize(entry, depth + 1, this._options!);

          if(IsNonEmptyJsonArrayOrObject(serializedValue)) {
            this.Append(serializedValue, 0);
          } else {
            this.Append(serializedValue, depth + 1);
          }

          this.Append(FieldSeparatorChar, 0);
          this.AppendLine();
          writeCount++;
        }

        // Output the end of the array and set the result
        this.RemoveLastComma();
        this.Append(CloseArrayChar, writeCount > 0 ? depth : 0);
        return this._builder!.ToString();
      }

      private void SetIndent(Int32 depth) {
        if(this._options!.Format == false || depth <= 0) {
          return;
        }

        _ = this._builder!.Append(IndentStrings.GetOrAdd(depth, x => new String(' ', x * 4)));
      }

      /// <summary>
      /// Removes the last comma in the current string builder.
      /// </summary>
      private void RemoveLastComma() {
        if(this._builder!.Length < this._lastCommaSearch!.Length) {
          return;
        }

        if(this._lastCommaSearch.Where((t, i) => this._builder[this._builder.Length - this._lastCommaSearch.Length + i] != t).Any()) {
          return;
        }

        // If we got this far, we simply remove the comma character
        _ = this._builder.Remove(this._builder.Length - this._lastCommaSearch.Length, 1);
      }

      private void Append(String text, Int32 depth) {
        this.SetIndent(depth);
        _ = this._builder!.Append(text);
      }

      private void Append(Char text, Int32 depth) {
        this.SetIndent(depth);
        _ = this._builder!.Append(text);
      }

      private void AppendLine() {
        if(this._options!.Format == false) {
          return;
        }

        _ = this._builder!.Append(Environment.NewLine);
      }

      #endregion
    }
  }
}