﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Text;
using System.Text.RegularExpressions;
using System.IO;
namespace CSML
{
    public static class Json
    {
        static int index;
        static string str;
        static StringBuilder builder = new StringBuilder();

        static Exception ReadError(string err)
        {
            int line = 1;
            int pos = 1;
            for (int i = 0; i < index; ++i)
            {
                if (str[i] == '\n')
                {
                    ++line;
                    pos = 1;
                }
                else
                    ++pos;
            }
            return new Exception($"({line}:{pos}) {err}");
        }

        static bool CanRead
        {
            get { return index < str.Length; }
        }

        static void SkipWhitespace()
        {
            while (CanRead && char.IsWhiteSpace(str[index]))
                ++index;
        }

        static string ParseWord()
        {
            builder.Clear();
            while (CanRead && char.IsLetter(str[index]))
                builder.Append(str[index++]);
            return builder.ToString();
        }

        //Returns true if the number is a float
        static bool ParseNumber(out int intVal, out float floatVal)
        {
            builder.Clear();

            if (str[index] == '-')
            {
                builder.Append('-');
                ++index;
            }

            while (CanRead && char.IsDigit(str[index]))
                builder.Append(str[index++]);
            if (index < str.Length + 1 && str[index] == '.' && char.IsDigit(str[index + 1]))
            {
                builder.Append('.');
                ++index;
                while (CanRead && char.IsDigit(str[index]))
                    builder.Append(str[index++]);
                intVal = 99;
                floatVal = float.Parse(builder.ToString());
                return true;
            }
            floatVal = 88f;
            intVal = int.Parse(builder.ToString());
            return false;
        }

        static string ParseString()
        {
            builder.Clear();

            char strChr = str[index];
            ++index;

            bool escape = false;
            while (CanRead)
            {
                if (escape)
                {
                    escape = false;
                    switch (str[index++])
                    {
                        case 'b':
                            builder.Append('\b');
                            break;
                        case 'f':
                            builder.Append('\f');
                            break;
                        case 'n':
                            builder.Append('\n');
                            break;
                        case 'r':
                            builder.Append('\r');
                            break;
                        case 't':
                            builder.Append('\t');
                            break;
                        case '"':
                            builder.Append('"');
                            break;
                        case '\\':
                            builder.Append('\\');
                            break;
                        default:
                            throw ReadError("Unexpected escape sequence: \\" + str[index - 1]);
                    }
                }
                else
                {
                    if (str[index] == '\\')
                    {
                        escape = true;
                        ++index;
                    }
                    else if (str[index] == strChr)
                    {
                        ++index;
                        return builder.ToString();
                    }
                    else
                        builder.Append(str[index++]);
                }
            }
            throw ReadError("Error parsing string.");
        }

        static object ParseValue()
        {
            SkipWhitespace();
            if (!CanRead)
                throw ReadError("Error parsing value.");
            object val = null;
            if (str[index] == '{')
                val = ParseObject();
            else if (str[index] == '[')
                val = ParseArray();
            else if (str[index] == '"' || str[index] == '\'')
                val = ParseString();
            else if (char.IsDigit(str[index]) || str[index] == '-')
            {
                int ival;
                float fval;
                if (ParseNumber(out ival, out fval))
                    val = fval;
                else
                    val = ival;
            }
            else if (char.IsLetter(str[index]))
            {
                string word = ParseWord();
                switch (word)
                {
                    case "true":
                        val = true;
                        break;
                    case "false":
                        val = false;
                        break;
                    case "null":
                        val = null;
                        break;
                    default:
                        throw ReadError("Unexpected identifier: " + word);
                }
            }
            else
                throw ReadError("Unexpected token when parsing value: " + str[index]);
            SkipWhitespace();
            return val;
        }

        static JsonArray ParseArray()
        {
            var arr = new JsonArray();

            ++index;
            SkipWhitespace();

            bool first = true;
            while (CanRead && str[index] != ']')
            {
                if (!first)
                {
                    if (str[index] != ',')
                        throw ReadError("Expected comma between array entries.");
                    ++index;
                    SkipWhitespace();
                }
                else
                    first = false;

                arr.Add(ParseValue());
            }

            if (!CanRead)
                throw ReadError("Error parsing array.");

            ++index;

            return arr;
        }

        static JsonObject ParseObject()
        {
            var obj = new JsonObject();

            ++index;
            SkipWhitespace();

            bool first = true;
            while (CanRead && str[index] != '}')
            {
                if (!first)
                {
                    if (str[index] != ',')
                        throw ReadError("Expected comma between array entries.");
                    ++index;
                    SkipWhitespace();
                }
                else
                    first = false;

                if (str[index] != '"' && str[index] != '\'')
                    throw new Exception("Expected string key.");

                string key = ParseString();

                SkipWhitespace();
                if (!CanRead || str[index] != ':')
                    throw new Exception("Expected ':'");
                ++index;
                SkipWhitespace();
                if (!CanRead)
                    throw ReadError("Error parsing object.");

                object val = ParseValue();
                obj.Set(key, val);

                SkipWhitespace();
            }

            if (!CanRead || str[index] != '}')
                throw ReadError("Error parsing object.");
            ++index;

            return obj;
        }

        public static JsonObject ParseObject(ref string str)
        {
            index = 0;
            Json.str = str;
            SkipWhitespace();
            if (!CanRead || str[index] != '{')
                throw ReadError("Error parsing object.");
            var obj = ParseObject();
            Json.str = null;
            return obj;
        }
        public static JsonObject ParseObject(string str)
        {
            return ParseObject(ref str);
        }
        public static JsonObject LoadObjectFile(string file)
        {
            index = 0;
            str = File.ReadAllText(file);
            SkipWhitespace();
            if (!CanRead || str[index] != '{')
                throw ReadError("Error parsing object.");
            var obj = ParseObject();
            str = null;
            return obj;
        }

        public static JsonArray ParseArray(ref string str)
        {
            index = 0;
            Json.str = str;
            SkipWhitespace();
            if (!CanRead || str[index] != '[')
                throw ReadError("Error parsing array.");
            var arr = ParseArray();
            Json.str = null;
            return arr;
        }
        public static JsonArray ParseArray(string str)
        {
            return ParseArray(ref str);
        }
        public static JsonArray LoadArrayFile(string file)
        {
            index = 0;
            str = File.ReadAllText(file);

            SkipWhitespace();
            if (!CanRead || str[index] != '[')
                throw ReadError("Error parsing array.");
            var arr = ParseArray();
            str = null;
            return arr;
        }

        static void AppendString(string text)
        {
            builder.Append('"');
            for (int i = 0; i < text.Length; ++i)
            {
                switch (text[i])
                {
                    case '\b':
                        builder.Append("\\b");
                        break;
                    case '\f':
                        builder.Append("\\f");
                        break;
                    case '\n':
                        builder.Append("\\n");
                        break;
                    case '\r':
                        builder.Append("\\r");
                        break;
                    case '\t':
                        builder.Append("\\t");
                        break;
                    case '"':
                        builder.Append("\\\"");
                        break;
                    case '\\':
                        builder.Append("\\\\");
                        break;
                    default:
                        builder.Append(text[i]);
                        break;
                }
            }
            builder.Append('"');
        }

        static void AppendValue(object val, ref string prefix)
        {
            if (val is int)
                builder.Append(val);
            else if (val is float)
            {
                string fl = val.ToString();
                if (fl.IndexOf('.') < 0)
                    fl += ".0";
                builder.Append(fl);
            }
            else if (val is string)
                AppendString((string)val);
            else if (val is bool)
                builder.Append(val.Equals(true) ? "true" : "false");
            else if (val is JsonObject)
                AppendObject((JsonObject)val, ref prefix);
            else if (val is JsonArray)
                AppendArray((JsonArray)val, ref prefix);
            else if (val == null)
                builder.Append("null");
            else
                throw new Exception("Unexpected value type in json: " + val.GetType());
        }

        static void AppendObject(JsonObject obj, ref string prefix)
        {
            builder.Append('{');
            if (obj.Count > 0)
            {
                string innerPrefix = prefix + "\t";
                builder.AppendLine();
                bool first = true;
                foreach (var pair in obj.Values)
                {
                    if (first)
                        first = false;
                    else
                        builder.Append(",\n");
                    builder.Append(innerPrefix);
                    AppendString(pair.Key);
                    builder.Append(": ");
                    AppendValue(pair.Value, ref innerPrefix);
                }
                builder.AppendLine();
                builder.Append(prefix);
            }
            builder.Append('}');
        }

        static void AppendArray(JsonArray arr, ref string prefix)
        {
            builder.Append('[');
            if (arr.Count > 0)
            {
                string innerPrefix = prefix + "\t";
                builder.AppendLine();
                bool first = true;
                for (int i = 0; i < arr.Count; ++i)
                {
                    if (first)
                        first = false;
                    else
                        builder.Append(",\n");
                    builder.Append(innerPrefix);
                    AppendValue(arr[i], ref innerPrefix);
                }
                builder.AppendLine();
                builder.Append(prefix);
            }
            builder.Append(']');
        }

        public static string Stringify(this JsonObject obj)
        {
            builder.Clear();
            string prefix = "";
            AppendObject(obj, ref prefix);
            return builder.ToString();
        }
        public static string Stringify(this JsonArray arr)
        {
            builder.Clear();
            string prefix = "";
            AppendArray(arr, ref prefix);
            return builder.ToString();
        }

        public static void SaveToFile(this JsonObject obj, string file)
        {
            builder.Clear();
            string prefix = "";
            AppendObject(obj, ref prefix);
            File.WriteAllText(file, builder.ToString());
        }
        public static void SaveToFile(this JsonArray arr, string file)
        {
            builder.Clear();
            string prefix = "";
            AppendArray(arr, ref prefix);
            File.WriteAllText(file, builder.ToString());
        }
    }

    public class JsonObject : IEnumerable<KeyValuePair<string, object>>
    {
        Dictionary<string, object> values;

        public JsonObject(int capacity)
        {
            values = new Dictionary<string, object>(capacity, StringComparer.Ordinal);
        }
        public JsonObject()
        {
            values = new Dictionary<string, object>(StringComparer.Ordinal);
        }

        public object this[string key]
        {
            get { return values[key]; }
            set { values[key] = value; }
        }

        public void Set(string key, object val)
        {
            values[key] = val;
        }
        public void Set(string key, object val, object def)
        {
            if (!val.Equals(def))
                values[key] = val;
        }

        public void Remove(string key)
        {
            values.Remove(key);
        }

        public bool Has(string key)
        {
            return values.ContainsKey(key);
        }

        public bool TryGet<T>(string key, out T val)
        {
            object obj;
            if (values.TryGetValue(key, out obj))
            {
                if (!(obj is T))
                {
                    throw new Exception($"Cannot cast {obj.GetType()} to {typeof(T)}.");
                }
                val = (T)obj;
                return true;
            }
            val = default(T);
            return false;
        }

        public T Get<T>(string key)
        {
            T val;
            if (!TryGet(key, out val))
                throw new ArgumentException("JsonObject does not have key: " + key, nameof(key));
            return val;
        }
        public T Get<T>(string key, T def)
        {
            T val;
            if (!TryGet(key, out val))
                return def;
            return val;
        }

        public void Clear()
        {
            values.Clear();
        }

        public IEnumerator<KeyValuePair<string, object>> GetEnumerator()
        {
            return values.GetEnumerator();
        }
        IEnumerator System.Collections.IEnumerable.GetEnumerator()
        {
            return values.GetEnumerator();
        }

        public int Count
        {
            get { return values.Count; }
        }

        public IEnumerable<KeyValuePair<string, object>> Values
        {
            get { return values; }
        }
    }

    public class JsonArray : IEnumerable
    {
        object[] values;
        public int Count { get; private set; }

        public JsonArray(int capacity)
        {
            values = new object[capacity];
        }
        public JsonArray() : this(4)
        {
            
        }

        public object this[int i]
        {
            get
            {
                if (i < 0 || i >= Count)
                    throw new ArgumentOutOfRangeException(nameof(i));
                return values[i];
            }
            set
            {
                if (i < 0 || i >= Count)
                    throw new ArgumentOutOfRangeException(nameof(i));
                values[i] = value;
            }
        }

        public void Resize(int count)
        {
            if (count < 0)
                throw new ArgumentException("Array size must be > 0", nameof(count));
            if (count > Count)
            {
                if (count > values.Length)
                {
                    int len = values.Length;
                    while (count > len)
                        len *= 2;
                    Array.Resize(ref values, len);
                }
                Count = count;
            }
            else if (count < Count)
            {
                for (int i = count; i < Count; ++i)
                    values[i] = null;
                Count = count;
            }
        }

        public void Add(object val)
        {
            if (Count == values.Length)
                Array.Resize(ref values, Count * 2);
            values[Count++] = val;
        }

        public object Get(int i)
        {
            if (i < 0 || i >= Count)
                throw new ArgumentOutOfRangeException(nameof(i));
            return values[i];
        }

        public T Get<T>(int i)
        {
            var obj = Get(i);
            if (!(obj is T))
                throw new Exception("Item at index " + i + " is not of type: " + typeof(T));
            return (T)obj;
        }

        public int IndexOf(object val)
        {
            for (int i = 0; i < Count; ++i)
                if (values[i].Equals(val))
                    return i;
            return -1;
        }

        public void RemoveAt(int i)
        {
            if (i < 0 || i >= Count)
                throw new ArgumentOutOfRangeException(nameof(i));
            if (i < Count - 1)
            {
                //Shift all values left after the removed value
                for (int j = i + 1; j < Count; ++j)
                    values[i] = values[j];
                values[Count - 1] = null;
            }
            else
                values[i] = null;
            --Count;
        }

        public void Remove(object val)
        {
            int i = IndexOf(val);
            if (i >= 0)
                RemoveAt(i);
        }

        public T FindFirst<T>(Predicate<T> pred)
        {
            for (int i = 0; i < Count; ++i)
                if (values[i] is T && pred((T)values[i]))
                    return (T)values[i];
            return default(T);
        }

        public void Clear()
        {
            for (int i = 0; i < Count; ++i)
                values[i] = null;
            Count = 0;
        }

        public IEnumerable<T> GetValues<T>()
        {
            for (int i = 0; i < Count; ++i)
                if (values[i] is T)
                    yield return (T)values[i];
        }

        public IEnumerator GetEnumerator()
        {
            return values.GetEnumerator();
        }

        public IEnumerable<object> Values
        {
            get { return values; }
        }

        public IEnumerable<JsonObject> Objects
        {
            get
            {
                for (int i = 0; i < Count; ++i)
                    if (values[i] is JsonObject)
                        yield return (JsonObject)values[i];
            }
        }
    }
}
