using CryptoExchange.Net.Attributes;
using CryptoExchange.Net.Converters.SystemTextJson;
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Linq;

namespace CryptoExchange.Net.Objects
{
    /// <summary>
    /// Parameters collection
    /// </summary>
    public class ParameterCollection : Dictionary<string, object>
    {
        /// <inheritdoc />
        public new void Add(string key, object value)
        {
            if (value == null)
                throw new ArgumentNullException(key);

            base.Add(key, value);
        }

        /// <summary>
        /// Add an optional parameter. Not added if value is null
        /// </summary>
        /// <param name="key"></param>
        /// <param name="value"></param>
        public void AddOptional(string key, object? value)
        {
            if (value != null)
                base.Add(key, value);
        }

        /// <summary>
        /// Add a decimal value as string
        /// </summary>
        /// <param name="key"></param>
        /// <param name="value"></param>
        public void AddString(string key, decimal value)
        {
            base.Add(key, value.ToString(CultureInfo.InvariantCulture));
        }

        /// <summary>
        /// Add a decimal value as string. Not added if value is null
        /// </summary>
        /// <param name="key"></param>
        /// <param name="value"></param>
        public void AddOptionalString(string key, decimal? value)
        {
            if (value != null)
                base.Add(key, value.Value.ToString(CultureInfo.InvariantCulture));
        }

        /// <summary>
        /// Add a int value as string
        /// </summary>
        /// <param name="key"></param>
        /// <param name="value"></param>
        public void AddString(string key, int value)
        {
            base.Add(key, value.ToString(CultureInfo.InvariantCulture));
        }

        /// <summary>
        /// Add a int value as string. Not added if value is null
        /// </summary>
        /// <param name="key"></param>
        /// <param name="value"></param>
        public void AddOptionalString(string key, int? value)
        {
            if (value != null)
                base.Add(key, value.Value.ToString(CultureInfo.InvariantCulture));
        }

        /// <summary>
        /// Add a long value as string
        /// </summary>
        /// <param name="key"></param>
        /// <param name="value"></param>
        public void AddString(string key, long value)
        {
            base.Add(key, value.ToString(CultureInfo.InvariantCulture));
        }

        /// <summary>
        /// Add a long value as string. Not added if value is null
        /// </summary>
        /// <param name="key"></param>
        /// <param name="value"></param>
        public void AddOptionalString(string key, long? value)
        {
            if (value != null)
                base.Add(key, value.Value.ToString(CultureInfo.InvariantCulture));
        }

        /// <summary>
        /// Add a DateTime value as string
        /// </summary>
        public void AddString(string key, DateTime value)
        {
            base.Add(key, value.ToString("yyyy-MM-ddTHH:mm:ssZ"));
        }

        /// <summary>
        /// Add a DateTime value as string. Not added if value is null
        /// </summary>
        public void AddOptionalString(string key, DateTime? value)
        {
            if (value != null)
                base.Add(key, value.Value.ToString("yyyy-MM-ddTHH:mm:ssZ"));
        }

        /// <summary>
        /// Add a datetime value as milliseconds timestamp
        /// </summary>
        /// <param name="key"></param>
        /// <param name="value"></param>
        public void AddMilliseconds(string key, DateTime value)
        {
            base.Add(key, DateTimeConverter.ConvertToMilliseconds(value));
        }

        /// <summary>
        /// Add a datetime value as milliseconds timestamp. Not added if value is null
        /// </summary>
        /// <param name="key"></param>
        /// <param name="value"></param>
        public void AddOptionalMilliseconds(string key, DateTime? value)
        {
            if (value != null)
                base.Add(key, DateTimeConverter.ConvertToMilliseconds(value));
        }

        /// <summary>
        /// Add a datetime value as milliseconds timestamp
        /// </summary>
        /// <param name="key"></param>
        /// <param name="value"></param>
        public void AddMillisecondsString(string key, DateTime value)
        {
            base.Add(key, DateTimeConverter.ConvertToMilliseconds(value).Value.ToString(CultureInfo.InvariantCulture));
        }

        /// <summary>
        /// Add a datetime value as milliseconds timestamp. Not added if value is null
        /// </summary>
        /// <param name="key"></param>
        /// <param name="value"></param>
        public void AddOptionalMillisecondsString(string key, DateTime? value)
        {
            if (value != null)
                base.Add(key, DateTimeConverter.ConvertToMilliseconds(value).Value.ToString(CultureInfo.InvariantCulture));
        }

        /// <summary>
        /// Add a datetime value as seconds timestamp
        /// </summary>
        /// <param name="key"></param>
        /// <param name="value"></param>
        public void AddSeconds(string key, DateTime value)
        {
            base.Add(key, DateTimeConverter.ConvertToSeconds(value));
        }

        /// <summary>
        /// Add a datetime value as seconds timestamp. Not added if value is null
        /// </summary>
        /// <param name="key"></param>
        /// <param name="value"></param>
        public void AddOptionalSeconds(string key, DateTime? value)
        {
            if (value != null)
                base.Add(key, DateTimeConverter.ConvertToSeconds(value));
        }

        /// <summary>
        /// Add a datetime value as string seconds timestamp
        /// </summary>
        /// <param name="key"></param>
        /// <param name="value"></param>
        public void AddSecondsString(string key, DateTime value)
        {
            base.Add(key, DateTimeConverter.ConvertToSeconds(value).ToString()!);
        }

        /// <summary>
        /// Add a datetime value as string seconds timestamp. Not added if value is null
        /// </summary>
        /// <param name="key"></param>
        /// <param name="value"></param>
        public void AddOptionalSecondsString(string key, DateTime? value)
        {
            if (value != null)
                base.Add(key, DateTimeConverter.ConvertToSeconds(value).ToString()!);
        }

        /// <summary>
        /// Add an enum value as the string value as mapped using the <see cref="MapAttribute" />
        /// </summary>
#if NET5_0_OR_GREATER
        public void AddEnum<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | DynamicallyAccessedMemberTypes.PublicFields)] T>(string key, T value)
#else
        public void AddEnum<T>(string key, T value)
#endif
            where T : struct, Enum
        {
            base.Add(key, EnumConverter<T>.GetString(value)!);
        }

        /// <summary>
        /// Add an enum value as the string value as mapped using the <see cref="MapAttribute" />
        /// </summary>
        /// <param name="key"></param>
        /// <param name="value"></param>
#if NET5_0_OR_GREATER
        public void AddEnumAsInt<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | DynamicallyAccessedMemberTypes.PublicFields)] T>(string key, T value)
#else
        public void AddEnumAsInt<T>(string key, T value)
#endif
            where T : struct, Enum
        {
            var stringVal = EnumConverter<T>.GetString(value)!;
            base.Add(key, int.Parse(stringVal)!);
        }

        /// <summary>
        /// Add an enum value as the string value as mapped using the <see cref="MapAttribute" />. Not added if value is null
        /// </summary>
        /// <param name="key"></param>
        /// <param name="value"></param>
#if NET5_0_OR_GREATER
        public void AddOptionalEnum<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | DynamicallyAccessedMemberTypes.PublicFields)] T>(string key, T? value)
#else
        public void AddOptionalEnum<T>(string key, T? value)
#endif
            where T : struct, Enum
        {
            if (value != null)
                base.Add(key, EnumConverter<T>.GetString(value));
        }

        /// <summary>
        /// Add an enum value as the string value as mapped using the <see cref="MapAttribute" />. Not added if value is null
        /// </summary>
#if NET5_0_OR_GREATER
        public void AddOptionalEnumAsInt<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | DynamicallyAccessedMemberTypes.PublicFields)] T>(string key, T? value)
#else
        public void AddOptionalEnumAsInt<T>(string key, T? value)
#endif
            where T : struct, Enum
        {
            if (value != null)
            {
                var stringVal = EnumConverter<T>.GetString(value);
                base.Add(key, int.Parse(stringVal));
            }
        }
        
        /// <summary>
        /// Add key as comma separated values
        /// </summary>
        public void AddCommaSeparated(string key, IEnumerable<string> values)
        {
            base.Add(key, string.Join(",", values));
        }

        /// <summary>
        /// Add key as comma separated values if there are values provided
        /// </summary>
        public void AddOptionalCommaSeparated(string key, IEnumerable<string>? values)
        {
            if (values == null || !values.Any())
                return;

            base.Add(key, string.Join(",", values));
        }

        /// <summary>
        /// Add key as comma separated values
        /// </summary>
#if NET5_0_OR_GREATER
        public void AddCommaSeparated<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | DynamicallyAccessedMemberTypes.PublicFields)] T>(string key, IEnumerable<T> values)
#else
        public void AddCommaSeparated<T>(string key, IEnumerable<T> values)
#endif
            where T : struct, Enum
        {
            base.Add(key, string.Join(",", values.Select(x => EnumConverter.GetString(x))));
        }

        /// <summary>
        /// Add key as comma separated values if there are values provided
        /// </summary>
#if NET5_0_OR_GREATER
        public void AddOptionalCommaSeparated<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | DynamicallyAccessedMemberTypes.PublicFields)] T>(string key, IEnumerable<T>? values)
#else
        public void AddOptionalCommaSeparated<T>(string key, IEnumerable<T>? values)
#endif
            where T : struct, Enum
        {
            if (values == null || !values.Any())
                return;

            base.Add(key, string.Join(",", values.Select(x => EnumConverter.GetString(x))));
        }

        /// <summary>
        /// Add key as boolean lower case value
        /// </summary>
        public void AddBoolString(string key, bool value)
        {
            base.Add(key, value.ToString().ToLower());
        }

        /// <summary>
        /// Add key as boolean lower case value if it's not null
        /// </summary>
        public void AddOptionalBoolString(string key, bool? value)
        {
            if (value == null)
                return;

            base.Add(key, value.ToString()!.ToLower());
        }


        /// <summary>
        /// Set the request body. Can be used to specify a simple value or array as the body instead of an object
        /// </summary>
        /// <param name="body">Body to set</param>
        /// <exception cref="InvalidOperationException"></exception>
        public void SetBody(object body)
        {
            if (this.Any())
                throw new InvalidOperationException("Can't set body when other parameters already specified");

            base.Add(Constants.BodyPlaceHolderKey, body);
        }
    }
}
