#nullable enable
using System;
using System.Collections.Generic;
using System.Linq;
using Swan.Collections;
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 {
    #region Constants 

    internal const String AddMethodName = "Add";

    private const Char OpenObjectChar = '{';
    private const Char CloseObjectChar = '}';

    private const Char OpenArrayChar = '[';
    private const Char CloseArrayChar = ']';

    private const Char FieldSeparatorChar = ',';
    private const Char ValueSeparatorChar = ':';

    private const Char StringEscapeChar = '\\';
    private const Char StringQuotedChar = '"';

    private const String EmptyObjectLiteral = "{ }";
    private const String EmptyArrayLiteral = "[ ]";
    private const String TrueLiteral = "true";
    private const String FalseLiteral = "false";
    private const String NullLiteral = "null";

    #endregion

    private static readonly CollectionCacheRepository<String> IgnoredPropertiesCache = new CollectionCacheRepository<global::System.String>();

    #region Public API

    /// <summary>
    /// Serializes the specified object into a JSON string.
    /// </summary>
    /// <param name="obj">The object.</param>
    /// <param name="format">if set to <c>true</c> it formats and indents the output.</param>
    /// <param name="typeSpecifier">The type specifier. Leave null or empty to avoid setting.</param>
    /// <param name="includeNonPublic">if set to <c>true</c> non-public getters will be also read.</param>
    /// <param name="includedNames">The included property names.</param>
    /// <param name="excludedNames">The excluded property names.</param>
    /// <returns>
    /// A <see cref="System.String" /> that represents the current object.
    /// </returns>
    /// <example>
    /// The following example describes how to serialize a simple object.
    /// <code>
    /// using Swan.Formatters;
    /// 
    /// class Example
    /// {
    ///     static void Main()
    ///     {
    ///         var obj = new { One = "One", Two = "Two" };
    ///         
    ///         var serial = Json.Serialize(obj); // {"One": "One","Two": "Two"}
    ///     }
    /// }
    /// </code>
    /// 
    /// The following example details how to serialize an object using the <see cref="JsonPropertyAttribute"/>.
    /// 
    /// <code>
    /// using Swan.Attributes;
    /// using Swan.Formatters;
    /// 
    /// class Example
    /// {
    ///     class JsonPropertyExample
    ///     {
    ///         [JsonProperty("data")]
    ///         public string Data { get; set; }
    ///         
    ///         [JsonProperty("ignoredData", true)]
    ///         public string IgnoredData { get; set; }
    ///     }
    ///     
    ///     static void Main()
    ///     {
    ///         var obj = new JsonPropertyExample() { Data = "OK", IgnoredData = "OK" };
    ///         
    ///         // {"data": "OK"}
    ///         var serializedObj = Json.Serialize(obj);
    ///     }
    /// }
    /// </code>
    /// </example>
    public static String Serialize(Object? obj, Boolean format = false, String? typeSpecifier = null, Boolean includeNonPublic = false, String[]? includedNames = null, params String[] excludedNames) => Serialize(obj, format, typeSpecifier, includeNonPublic, includedNames, excludedNames, null, JsonSerializerCase.None);

    /// <summary>
    /// Serializes the specified object into a JSON string.
    /// </summary>
    /// <param name="obj">The object.</param>
    /// <param name="format">if set to <c>true</c> it formats and indents the output.</param>
    /// <param name="typeSpecifier">The type specifier. Leave null or empty to avoid setting.</param>
    /// <param name="includeNonPublic">if set to <c>true</c> non-public getters will be also read.</param>
    /// <param name="includedNames">The included property names.</param>
    /// <param name="excludedNames">The excluded property names.</param>
    /// <param name="parentReferences">The parent references.</param>
    /// <param name="jsonSerializerCase">The json serializer case.</param>
    /// <returns>
    /// A <see cref="System.String" /> that represents the current object.
    /// </returns>
    public static String Serialize(Object? obj, Boolean format, String? typeSpecifier, Boolean includeNonPublic, String[]? includedNames, String[]? excludedNames, List<WeakReference>? parentReferences, JsonSerializerCase jsonSerializerCase) {
      if(obj != null && (obj is String || Definitions.AllBasicValueTypes.Contains(obj.GetType()))) {
        return SerializePrimitiveValue(obj);
      }

      SerializerOptions options = new SerializerOptions(format, typeSpecifier, includedNames, GetExcludedNames(obj?.GetType(), excludedNames), includeNonPublic, parentReferences, jsonSerializerCase);

      return Serialize(obj, options);
    }

    /// <summary>
    /// Serializes the specified object using the SerializerOptions provided.
    /// </summary>
    /// <param name="obj">The object.</param>
    /// <param name="options">The options.</param>
    /// <returns>
    /// A <see cref="String" /> that represents the current object.
    /// </returns>
    public static String Serialize(Object? obj, SerializerOptions options) => Serializer.Serialize(obj, 0, options);

    /// <summary>
    /// Deserializes the specified json string as either a Dictionary[string, object] or as a List[object]
    /// depending on the syntax of the JSON string.
    /// </summary>
    /// <param name="json">The JSON string.</param>
    /// <param name="jsonSerializerCase">The json serializer case.</param>
    /// <returns>
    /// Type of the current deserializes.
    /// </returns>
    /// <example>
    /// The following code shows how to deserialize a JSON string into a Dictionary.
    /// <code>
    /// using Swan.Formatters;
    /// class Example
    /// {
    /// static void Main()
    /// {
    /// // json to deserialize
    /// var basicJson = "{\"One\":\"One\",\"Two\":\"Two\",\"Three\":\"Three\"}";
    /// // deserializes the specified json into a Dictionary&lt;string, object&gt;.
    /// var data = Json.Deserialize(basicJson, JsonSerializerCase.None);
    /// }
    /// }
    /// </code></example>
    public static Object? Deserialize(String? json, JsonSerializerCase jsonSerializerCase) => Converter.FromJsonResult(Deserializer.DeserializeInternal(json), jsonSerializerCase);

    /// <summary>
    /// Deserializes the specified json string as either a Dictionary[string, object] or as a List[object]
    /// depending on the syntax of the JSON string.
    /// </summary>
    /// <param name="json">The JSON string.</param>
    /// <returns>
    /// Type of the current deserializes.
    /// </returns>
    /// <example>
    /// The following code shows how to deserialize a JSON string into a Dictionary.
    /// <code>
    /// using Swan.Formatters;
    /// class Example
    /// {
    /// static void Main()
    /// {
    /// // json to deserialize
    /// var basicJson = "{\"One\":\"One\",\"Two\":\"Two\",\"Three\":\"Three\"}";
    /// // deserializes the specified json into a Dictionary&lt;string, object&gt;.
    /// var data = Json.Deserialize(basicJson);
    /// }
    /// }
    /// </code></example>
    public static Object? Deserialize(String? json) => Deserialize(json, JsonSerializerCase.None);

    #endregion

    #region Private API

    private static String[]? GetExcludedNames(Type? type, String[]? excludedNames) {
      if(type == null) {
        return excludedNames;
      }

      IEnumerable<global::System.String> excludedByAttr = IgnoredPropertiesCache.Retrieve(type, t => t.GetProperties().Where(x => AttributeCache.DefaultCache.Value.RetrieveOne<JsonPropertyAttribute>(x)?.Ignored == true).Select(x => x.Name));

      return excludedByAttr?.Any() != true ? excludedNames : excludedNames?.Any(String.IsNullOrWhiteSpace) == true ? excludedByAttr.Intersect(excludedNames.Where(y => !String.IsNullOrWhiteSpace(y))).ToArray() : excludedByAttr.ToArray();
    }

    private static String SerializePrimitiveValue(Object obj) => obj switch
    {
      String stringValue => stringValue,
      Boolean boolValue => boolValue ? TrueLiteral : FalseLiteral,
      _ => obj.ToString()!
    };

    #endregion
  }
}