using System;
using System.Collections.Generic;
using System.Diagnostics;

namespace Unity.VisualScripting.FullSerializer
{
    /// <summary>
    /// The actual type that a JsonData instance can store.
    /// </summary>
    public enum fsDataType
    {
        Array,
        Object,
        Double,
        Int64,
        Boolean,
        String,
        Null
    }

    /// <summary>
    /// A union type that stores a serialized value. The stored type can be one
    /// of six different
    /// types: null, boolean, double, Int64, string, Dictionary, or List.
    /// </summary>
    public sealed class fsData
    {
        /// <summary>
        /// The raw value that this serialized data stores. It can be one of six
        /// different types; a boolean, a double, Int64, a string, a Dictionary,
        /// or a List.
        /// </summary>
        private object _value;

        #region ToString Implementation

        public override string ToString()
        {
            return fsJsonPrinter.CompressedJson(this);
        }

        #endregion ToString Implementation

        #region Constructors

        /// <summary>
        /// Creates a fsData instance that holds null.
        /// </summary>
        public fsData()
        {
            _value = null;
        }

        /// <summary>
        /// Creates a fsData instance that holds a boolean.
        /// </summary>
        public fsData(bool boolean)
        {
            _value = boolean;
        }

        /// <summary>
        /// Creates a fsData instance that holds a double.
        /// </summary>
        public fsData(double f)
        {
            _value = f;
        }

        /// <summary>
        /// Creates a new fsData instance that holds an integer.
        /// </summary>
        public fsData(Int64 i)
        {
            _value = i;
        }

        /// <summary>
        /// Creates a fsData instance that holds a string.
        /// </summary>
        public fsData(string str)
        {
            _value = str;
        }

        /// <summary>
        /// Creates a fsData instance that holds a dictionary of values.
        /// </summary>
        public fsData(Dictionary<string, fsData> dict)
        {
            _value = dict;
        }

        /// <summary>
        /// Creates a fsData instance that holds a list of values.
        /// </summary>
        public fsData(List<fsData> list)
        {
            _value = list;
        }

        /// <summary>
        /// Helper method to create a fsData instance that holds a dictionary.
        /// </summary>
        public static fsData CreateDictionary()
        {
            return new fsData(new Dictionary<string, fsData>(
                fsGlobalConfig.IsCaseSensitive ? StringComparer.Ordinal : StringComparer.OrdinalIgnoreCase));
        }

        /// <summary>
        /// Helper method to create a fsData instance that holds a list.
        /// </summary>
        public static fsData CreateList()
        {
            return new fsData(new List<fsData>());
        }

        /// <summary>
        /// Helper method to create a fsData instance that holds a list with the
        /// initial capacity.
        /// </summary>
        public static fsData CreateList(int capacity)
        {
            return new fsData(new List<fsData>(capacity));
        }

        public readonly static fsData True = new fsData(true);
        public readonly static fsData False = new fsData(false);
        public readonly static fsData Null = new fsData();

        #endregion Constructors

        #region Internal Helper Methods

        /// <summary>
        /// Transforms the internal fsData instance into a dictionary.
        /// </summary>
        internal void BecomeDictionary()
        {
            _value = new Dictionary<string, fsData>();
        }

        /// <summary>
        /// Returns a shallow clone of this data instance.
        /// </summary>
        internal fsData Clone()
        {
            var clone = new fsData();
            clone._value = _value;
            return clone;
        }

        #endregion Internal Helper Methods

        #region Casting Predicates

        public fsDataType Type
        {
            get
            {
                if (_value == null)
                {
                    return fsDataType.Null;
                }
                if (_value is double)
                {
                    return fsDataType.Double;
                }
                if (_value is Int64)
                {
                    return fsDataType.Int64;
                }
                if (_value is bool)
                {
                    return fsDataType.Boolean;
                }
                if (_value is string)
                {
                    return fsDataType.String;
                }
                if (_value is Dictionary<string, fsData>)
                {
                    return fsDataType.Object;
                }
                if (_value is List<fsData>)
                {
                    return fsDataType.Array;
                }

                throw new InvalidOperationException("unknown JSON data type");
            }
        }

        /// <summary>
        /// Returns true if this fsData instance maps back to null.
        /// </summary>
        public bool IsNull => _value == null;

        /// <summary>
        /// Returns true if this fsData instance maps back to a double.
        /// </summary>
        public bool IsDouble => _value is double;

        /// <summary>
        /// Returns true if this fsData instance maps back to an Int64.
        /// </summary>
        public bool IsInt64 => _value is Int64;

        /// <summary>
        /// Returns true if this fsData instance maps back to a boolean.
        /// </summary>
        public bool IsBool => _value is bool;

        /// <summary>
        /// Returns true if this fsData instance maps back to a string.
        /// </summary>
        public bool IsString => _value is string;

        /// <summary>
        /// Returns true if this fsData instance maps back to a Dictionary.
        /// </summary>
        public bool IsDictionary => _value is Dictionary<string, fsData>;

        /// <summary>
        /// Returns true if this fsData instance maps back to a List.
        /// </summary>
        public bool IsList => _value is List<fsData>;

        #endregion Casting Predicates

        #region Casts

        /// <summary>
        /// Casts this fsData to a double. Throws an exception if it is not a
        /// double.
        /// </summary>
        [DebuggerBrowsable(DebuggerBrowsableState.Never)]
        public double AsDouble => Cast<double>();

        /// <summary>
        /// Casts this fsData to an Int64. Throws an exception if it is not an
        /// Int64.
        /// </summary>
        [DebuggerBrowsable(DebuggerBrowsableState.Never)]
        public Int64 AsInt64 => Cast<Int64>();

        /// <summary>
        /// Casts this fsData to a boolean. Throws an exception if it is not a
        /// boolean.
        /// </summary>
        [DebuggerBrowsable(DebuggerBrowsableState.Never)]
        public bool AsBool => Cast<bool>();

        /// <summary>
        /// Casts this fsData to a string. Throws an exception if it is not a
        /// string.
        /// </summary>
        [DebuggerBrowsable(DebuggerBrowsableState.Never)]
        public string AsString => Cast<string>();

        /// <summary>
        /// Casts this fsData to a Dictionary. Throws an exception if it is not a
        /// Dictionary.
        /// </summary>
        [DebuggerBrowsable(DebuggerBrowsableState.Never)]
        public Dictionary<string, fsData> AsDictionary => Cast<Dictionary<string, fsData>>();

        /// <summary>
        /// Casts this fsData to a List. Throws an exception if it is not a List.
        /// </summary>
        [DebuggerBrowsable(DebuggerBrowsableState.Never)]
        public List<fsData> AsList => Cast<List<fsData>>();

        /// <summary>
        /// Internal helper method to cast the underlying storage to the given
        /// type or throw a pretty printed exception on failure.
        /// </summary>
        private T Cast<T>()
        {
            if (_value is T)
            {
                return (T)_value;
            }

            throw new InvalidCastException("Unable to cast <" + this + "> (with type = " +
                _value.GetType() + ") to type " + typeof(T));
        }

        #endregion Casts

        #region Equality Comparisons

        /// <summary>
        /// Determines whether the specified object is equal to the current
        /// object.
        /// </summary>
        public override bool Equals(object obj)
        {
            return Equals(obj as fsData);
        }

        /// <summary>
        /// Determines whether the specified object is equal to the current
        /// object.
        /// </summary>
        public bool Equals(fsData other)
        {
            if (other == null || Type != other.Type)
            {
                return false;
            }

            switch (Type)
            {
                case fsDataType.Null:
                    return true;

                case fsDataType.Double:
                    return AsDouble == other.AsDouble || Math.Abs(AsDouble - other.AsDouble) < double.Epsilon;

                case fsDataType.Int64:
                    return AsInt64 == other.AsInt64;

                case fsDataType.Boolean:
                    return AsBool == other.AsBool;

                case fsDataType.String:
                    return AsString == other.AsString;

                case fsDataType.Array:
                    var thisList = AsList;
                    var otherList = other.AsList;

                    if (thisList.Count != otherList.Count)
                    {
                        return false;
                    }

                    for (var i = 0; i < thisList.Count; ++i)
                    {
                        if (thisList[i].Equals(otherList[i]) == false)
                        {
                            return false;
                        }
                    }

                    return true;

                case fsDataType.Object:
                    var thisDict = AsDictionary;
                    var otherDict = other.AsDictionary;

                    if (thisDict.Count != otherDict.Count)
                    {
                        return false;
                    }

                    foreach (var key in thisDict.Keys)
                    {
                        if (otherDict.ContainsKey(key) == false)
                        {
                            return false;
                        }

                        if (thisDict[key].Equals(otherDict[key]) == false)
                        {
                            return false;
                        }
                    }

                    return true;
            }

            throw new Exception("Unknown data type");
        }

        /// <summary>
        /// Returns true iff a == b.
        /// </summary>
        public static bool operator ==(fsData a, fsData b)
        {
            // If both are null, or both are same instance, return true.
            if (ReferenceEquals(a, b))
            {
                return true;
            }

            // If one is null, but not both, return false.
            if (((object)a == null) || ((object)b == null))
            {
                return false;
            }

            if (a.IsDouble && b.IsDouble)
            {
                return Math.Abs(a.AsDouble - b.AsDouble) < double.Epsilon;
            }

            return a.Equals(b);
        }

        /// <summary>
        /// Returns true iff a != b.
        /// </summary>
        public static bool operator !=(fsData a, fsData b)
        {
            return !(a == b);
        }

        /// <summary>
        /// Returns a hash code for this instance.
        /// </summary>
        /// <returns>
        /// A hash code for this instance, suitable for use in hashing algorithms
        /// and data structures like a hash table.
        /// </returns>
        public override int GetHashCode()
        {
            return _value.GetHashCode();
        }

        #endregion Equality Comparisons
    }
}