using System;
using System.Collections.Generic;
using System.Globalization;
using System.Text;

namespace MajacloudSdk.Internal
{
    internal enum JsonType { Null, Bool, Number, String, Array, Object }

    /// <summary>
    /// Minimal JSON DOM. JsonUtility cannot express nullable ranks, nested
    /// nulls or arbitrary documents, so API envelopes go through this type.
    /// </summary>
    internal sealed class JsonNode
    {
        public JsonType Type;
        public bool BoolValue;
        public double NumberValue;
        public bool IsInteger;
        public long LongValue;
        public string StringValue;
        public List<JsonNode> Items;
        public List<KeyValuePair<string, JsonNode>> Fields;

        public static readonly JsonNode Null = new JsonNode { Type = JsonType.Null };

        public bool IsNull => Type == JsonType.Null;
        public bool IsObject => Type == JsonType.Object;
        public bool IsArray => Type == JsonType.Array;
        public int Count => Type == JsonType.Array ? Items.Count : Type == JsonType.Object ? Fields.Count : 0;

        public static JsonNode Parse(string json) => JsonParser.Parse(json);

        public static bool TryParse(string json, out JsonNode node, out string error)
        {
            try
            {
                node = JsonParser.Parse(json);
                error = null;
                return true;
            }
            catch (JsonParseException e)
            {
                node = null;
                error = e.Message;
                return false;
            }
        }

        public JsonNode this[string key]
        {
            get
            {
                if (Type != JsonType.Object || key == null) return Null;
                for (int i = 0; i < Fields.Count; i++)
                {
                    if (Fields[i].Key == key) return Fields[i].Value ?? Null;
                }
                return Null;
            }
        }

        public JsonNode this[int index]
        {
            get
            {
                if (Type != JsonType.Array || index < 0 || index >= Items.Count) return Null;
                return Items[index] ?? Null;
            }
        }

        public bool Has(string key)
        {
            if (Type != JsonType.Object) return false;
            for (int i = 0; i < Fields.Count; i++)
            {
                if (Fields[i].Key == key) return true;
            }
            return false;
        }

        public string GetString(string key, string fallback = null)
        {
            var node = this[key];
            switch (node.Type)
            {
                case JsonType.String: return node.StringValue;
                case JsonType.Number: return node.IsInteger ? node.LongValue.ToString(CultureInfo.InvariantCulture) : node.NumberValue.ToString("R", CultureInfo.InvariantCulture);
                case JsonType.Bool: return node.BoolValue ? "true" : "false";
                default: return fallback;
            }
        }

        public double GetDouble(string key, double fallback = 0)
        {
            var node = this[key];
            if (node.Type == JsonType.Number) return node.NumberValue;
            if (node.Type == JsonType.String && double.TryParse(node.StringValue, NumberStyles.Float, CultureInfo.InvariantCulture, out var parsed)) return parsed;
            if (node.Type == JsonType.Bool) return node.BoolValue ? 1 : 0;
            return fallback;
        }

        public double? GetNullableDouble(string key)
        {
            var node = this[key];
            return node.Type == JsonType.Number ? node.NumberValue : (double?)null;
        }

        public long GetLong(string key, long fallback = 0)
        {
            var node = this[key];
            if (node.Type == JsonType.Number) return node.IsInteger ? node.LongValue : (long)Math.Round(node.NumberValue);
            if (node.Type == JsonType.String && long.TryParse(node.StringValue, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsed)) return parsed;
            return fallback;
        }

        public int GetInt(string key, int fallback = 0)
        {
            var value = GetLong(key, fallback);
            if (value > int.MaxValue) return int.MaxValue;
            if (value < int.MinValue) return int.MinValue;
            return (int)value;
        }

        public int? GetNullableInt(string key)
        {
            var node = this[key];
            return node.Type == JsonType.Number ? (int?)(node.IsInteger ? (int)node.LongValue : (int)Math.Round(node.NumberValue)) : null;
        }

        public bool GetBool(string key, bool fallback = false)
        {
            var node = this[key];
            if (node.Type == JsonType.Bool) return node.BoolValue;
            if (node.Type == JsonType.Number) return Math.Abs(node.NumberValue) > double.Epsilon;
            if (node.Type == JsonType.String)
            {
                var s = node.StringValue.Trim().ToLowerInvariant();
                if (s == "true" || s == "1" || s == "yes") return true;
                if (s == "false" || s == "0" || s == "no" || s == "") return false;
            }
            return fallback;
        }

        public DateTime? GetDateTime(string key)
        {
            var text = GetString(key);
            if (string.IsNullOrEmpty(text)) return null;
            return DateTime.TryParse(text, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal | DateTimeStyles.AssumeUniversal, out var parsed) ? parsed : (DateTime?)null;
        }

        public JsonNode GetObject(string key)
        {
            var node = this[key];
            return node.Type == JsonType.Object ? node : Null;
        }

        public JsonNode GetArray(string key)
        {
            var node = this[key];
            return node.Type == JsonType.Array ? node : Null;
        }

        public IEnumerable<JsonNode> Elements()
        {
            if (Type != JsonType.Array) yield break;
            for (int i = 0; i < Items.Count; i++) yield return Items[i] ?? Null;
        }

        // --- Builders -------------------------------------------------------------

        public static JsonNode Object() => new JsonNode { Type = JsonType.Object, Fields = new List<KeyValuePair<string, JsonNode>>() };
        public static JsonNode Array() => new JsonNode { Type = JsonType.Array, Items = new List<JsonNode>() };
        public static JsonNode From(string value) => value == null ? Null : new JsonNode { Type = JsonType.String, StringValue = value };
        public static JsonNode From(bool value) => new JsonNode { Type = JsonType.Bool, BoolValue = value };
        public static JsonNode From(long value) => new JsonNode { Type = JsonType.Number, NumberValue = value, LongValue = value, IsInteger = true };
        public static JsonNode From(int value) => From((long)value);
        public static JsonNode From(double value)
        {
            var node = new JsonNode { Type = JsonType.Number, NumberValue = value };
            if (Math.Abs(value) < 9.007199254740992E15 && Math.Abs(value % 1) < double.Epsilon)
            {
                node.IsInteger = true;
                node.LongValue = (long)value;
            }
            return node;
        }

        /// <summary>Embed a pre-serialised JSON fragment verbatim (user payloads from JsonUtility).</summary>
        public static JsonNode Raw(string json)
        {
            if (string.IsNullOrWhiteSpace(json)) return Null;
            return Parse(json);
        }

        public JsonNode Set(string key, JsonNode value)
        {
            if (Type != JsonType.Object) throw new InvalidOperationException("Set() requires an object node.");
            for (int i = 0; i < Fields.Count; i++)
            {
                if (Fields[i].Key == key)
                {
                    Fields[i] = new KeyValuePair<string, JsonNode>(key, value ?? Null);
                    return this;
                }
            }
            Fields.Add(new KeyValuePair<string, JsonNode>(key, value ?? Null));
            return this;
        }

        public JsonNode Set(string key, string value) => Set(key, From(value));
        public JsonNode Set(string key, double value) => Set(key, From(value));
        public JsonNode Set(string key, long value) => Set(key, From(value));
        public JsonNode Set(string key, bool value) => Set(key, From(value));

        public JsonNode Add(JsonNode value)
        {
            if (Type != JsonType.Array) throw new InvalidOperationException("Add() requires an array node.");
            Items.Add(value ?? Null);
            return this;
        }

        public string ToJson()
        {
            var sb = new StringBuilder();
            JsonWriter.Write(this, sb);
            return sb.ToString();
        }

        public override string ToString() => ToJson();
    }

    internal sealed class JsonParseException : Exception
    {
        public int Position { get; }

        public JsonParseException(string message, int position) : base(message + " at position " + position)
        {
            Position = position;
        }
    }
}
