﻿#define PRETTY		//Comment out when you no longer need to read JSON to disable pretty print system-wide
//#define USEFLOAT	//Use floats for numbers instead of doubles	(enable if you're getting too many significant digits in string output)

using UnityEngine;
using System.Collections;
using System.Collections.Generic;

/*
 * http://www.opensource.org/licenses/lgpl-2.1.php
 * JSONObject class v.1.4.1
 * for use with Unity
 * Copyright Matt Schoen 2010 - 2013
 */

public class JSONObject
{
    const int MAX_DEPTH = 1000;
    const string INFINITY = "\"INFINITY\"";
    const string NEGINFINITY = "\"NEGINFINITY\"";
    const string NaN = "\"NaN\"";
    public static char[] WHITESPACE = new char[] { ' ', '\r', '\n', '\t' };
    public enum Type { NULL, STRING, NUMBER, OBJECT, ARRAY, BOOL }
    public bool isContainer { get { return (type == Type.ARRAY || type == Type.OBJECT); } }
    public JSONObject parent;
    public Type type = Type.NULL;
    public int Count
    {
        get
        {
            if (list == null)
                return -1;
            return list.Count;
        }
    }
    //TODO: Switch to list
    public List<JSONObject> list;
    public List<string> keys;
    public string str;
#if USEFLOAT
    public float n;
    public float f
    {
        get
        {
            return n;
        }
    }
#else
	public double n;
	public float f {
		get {
			return (float)n;
		}
	}
#endif
    public bool b;
    public delegate void AddJSONConents(JSONObject self);

    public static JSONObject nullJO { get { return new JSONObject(JSONObject.Type.NULL); } }	//an empty, null object
    public static JSONObject obj { get { return new JSONObject(JSONObject.Type.OBJECT); } }		//an empty object
    public static JSONObject arr { get { return new JSONObject(JSONObject.Type.ARRAY); } }		//an empty array

    public JSONObject(JSONObject.Type t)
    {
        type = t;
        switch (t)
        {
            case Type.ARRAY:
                list = new List<JSONObject>();
                break;
            case Type.OBJECT:
                list = new List<JSONObject>();
                keys = new List<string>();
                break;
        }
    }
    public JSONObject(bool b)
    {
        type = Type.BOOL;
        this.b = b;
    }
    public JSONObject(float f)
    {
        type = Type.NUMBER;
        this.n = f;
    }
    public JSONObject(Dictionary<string, string> dic)
    {
        type = Type.OBJECT;
        keys = new List<string>();
        list = new List<JSONObject>();
        foreach (KeyValuePair<string, string> kvp in dic)
        {
            keys.Add(kvp.Key);
            list.Add(new JSONObject { type = Type.STRING, str = kvp.Value });
        }
    }
    public JSONObject(Dictionary<string, JSONObject> dic)
    {
        type = Type.OBJECT;
        keys = new List<string>();
        list = new List<JSONObject>();
        foreach (KeyValuePair<string, JSONObject> kvp in dic)
        {
            keys.Add(kvp.Key);
            list.Add(kvp.Value);
        }
    }
    public JSONObject(AddJSONConents content)
    {
        content.Invoke(this);
    }
    public JSONObject(JSONObject[] objs)
    {
        type = Type.ARRAY;
        list = new List<JSONObject>(objs);
    }
    //Convenience function for creating a JSONObject containing a string.  This is not part of the constructor so that malformed JSON data doesn't just turn into a string object
    public static JSONObject StringObject(string val) { return new JSONObject { type = JSONObject.Type.STRING, str = val }; }
    public void Absorb(JSONObject obj)
    {
        list.AddRange(obj.list);
        keys.AddRange(obj.keys);
        str = obj.str;
        n = obj.n;
        b = obj.b;
        type = obj.type;
    }
    public JSONObject() { }
    #region PARSE
    public JSONObject(string str, bool strict = false)
    {	//create a new JSONObject from a string (this will also create any children, and parse the whole string)
        if (str != null)
        {
            str = str.Trim(WHITESPACE);
            if (strict)
            {
                if (str[0] != '[' && str[0] != '{')
                {
                    type = Type.NULL;
                    Debug.LogWarning("Improper (strict) JSON formatting.  First character must be [ or {");
                    return;
                }
            }
            if (str.Length > 0)
            {
                if (string.Compare(str, "true", true) == 0)
                {
                    type = Type.BOOL;
                    b = true;
                }
                else if (string.Compare(str, "false", true) == 0)
                {
                    type = Type.BOOL;
                    b = false;
                }
                else if (string.Compare(str, "null", true) == 0)
                {
                    type = Type.NULL;
#if USEFLOAT
                }
                else if (str == INFINITY)
                {
                    type = Type.NUMBER;
                    n = float.PositiveInfinity;
                }
                else if (str == NEGINFINITY)
                {
                    type = Type.NUMBER;
                    n = float.NegativeInfinity;
                }
                else if (str == NaN)
                {
                    type = Type.NUMBER;
                    n = float.NaN;
#else
				} else if(str == INFINITY) {
					type = Type.NUMBER;
					n = double.PositiveInfinity;
				} else if(str == NEGINFINITY) {
					type = Type.NUMBER;
					n = double.NegativeInfinity;
				} else if(str == NaN) {
					type = Type.NUMBER;
					n = double.NaN;
#endif
                }
                else if (str[0] == '"')
                {
                    type = Type.STRING;
                    this.str = str.Substring(1, str.Length - 2);
                }
                else
                {
                    try
                    {
#if USEFLOAT
                        n = System.Convert.ToSingle(str);
#else
						n = System.Convert.ToDouble(str);				 
#endif
                        type = Type.NUMBER;
                    }
                    catch (System.FormatException)
                    {
                        int token_tmp = 1;
                        /*
                         * Checking for the following formatting (www.json.org)
                         * object - {"field1":value,"field2":value}
                         * array - [value,value,value]
                         * value - string	- "string"
                         *		 - number	- 0.0
                         *		 - bool		- true -or- false
                         *		 - null		- null
                         */
                        int offset = 0;
                        switch (str[offset])
                        {
                            case '{':
                                type = Type.OBJECT;
                                keys = new List<string>();
                                list = new List<JSONObject>();
                                break;
                            case '[':
                                type = JSONObject.Type.ARRAY;
                                list = new List<JSONObject>();
                                break;
                            default:
                                type = Type.NULL;
                                Debug.LogWarning("improper JSON formatting:" + str);
                                return;
                        }
                        string propName = "";
                        bool openQuote = false;
                        bool inProp = false;
                        int depth = 0;
                        while (++offset < str.Length)
                        {
                            if (System.Array.IndexOf<char>(WHITESPACE, str[offset]) > -1)
                                continue;
                            if (str[offset] == '\"')
                            {
                                if (openQuote)
                                {
                                    if (!inProp && depth == 0 && type == Type.OBJECT)
                                        propName = str.Substring(token_tmp + 1, offset - token_tmp - 1);
                                    openQuote = false;
                                }
                                else
                                {
                                    if (depth == 0 && type == Type.OBJECT)
                                        token_tmp = offset;
                                    openQuote = true;
                                }
                            }
                            if (openQuote)
                                continue;
                            if (type == Type.OBJECT && depth == 0)
                            {
                                if (str[offset] == ':')
                                {
                                    token_tmp = offset + 1;
                                    inProp = true;
                                }
                            }

                            if (str[offset] == '[' || str[offset] == '{')
                            {
                                depth++;
                            }
                            else if (str[offset] == ']' || str[offset] == '}')
                            {
                                depth--;
                            }
                            //if  (encounter a ',' at top level)  || a closing ]/}
                            if ((str[offset] == ',' && depth == 0) || depth < 0)
                            {
                                inProp = false;
                                string inner = str.Substring(token_tmp, offset - token_tmp).Trim(WHITESPACE);
                                if (inner.Length > 0)
                                {
                                    if (type == Type.OBJECT)
                                        keys.Add(propName);
                                    list.Add(new JSONObject(inner));
                                }
                                token_tmp = offset + 1;
                            }
                        }
                    }
                }
            }
            else type = Type.NULL;
        }
        else type = Type.NULL;	//If the string is missing, this is a null
    }
    #endregion
    public bool IsNumber { get { return type == Type.NUMBER; } }
    public bool IsNull { get { return type == Type.NULL; } }
    public bool IsString { get { return type == Type.STRING; } }
    public bool IsBool { get { return type == Type.BOOL; } }
    public bool IsArray { get { return type == Type.ARRAY; } }
    public bool IsObject { get { return type == Type.OBJECT; } }
    public void Add(bool val) { Add(new JSONObject(val)); }
    public void Add(float val) { Add(new JSONObject(val)); }
    public void Add(int val) { Add(new JSONObject(val)); }
    public void Add(string str) { Add(StringObject(str)); }
    public void Add(AddJSONConents content) { Add(new JSONObject(content)); }
    public void Add(JSONObject obj)
    {
        if (obj)
        {		//Don't do anything if the object is null
            if (type != JSONObject.Type.ARRAY)
            {
                type = JSONObject.Type.ARRAY;		//Congratulations, son, you're an ARRAY now
                if (list == null)
                    list = new List<JSONObject>();
            }
            list.Add(obj);
        }
    }
    public void AddField(string name, bool val) { AddField(name, new JSONObject(val)); }
    public void AddField(string name, float val) { AddField(name, new JSONObject(val)); }
    public void AddField(string name, int val) { AddField(name, new JSONObject(val)); }
    public void AddField(string name, AddJSONConents content) { AddField(name, new JSONObject(content)); }
    public void AddField(string name, string val) { AddField(name, StringObject(val)); }
    public void AddField(string name, JSONObject obj)
    {
        if (obj)
        {		//Don't do anything if the object is null
            if (type != JSONObject.Type.OBJECT)
            {
                keys = new List<string>();
                if (type == Type.ARRAY)
                {
                    for (int i = 0; i < list.Count; i++)
                        keys.Add(i + "");
                }
                else if (list == null)
                    list = new List<JSONObject>();
                type = JSONObject.Type.OBJECT;		//Congratulations, son, you're an OBJECT now
            }
            keys.Add(name);
            list.Add(obj);
        }
    }
    public void SetField(string name, bool val) { SetField(name, new JSONObject(val)); }
    public void SetField(string name, float val) { SetField(name, new JSONObject(val)); }
    public void SetField(string name, int val) { SetField(name, new JSONObject(val)); }
    public void SetField(string name, JSONObject obj)
    {
        if (HasField(name))
        {
            list.Remove(this[name]);
            keys.Remove(name);
        }
        AddField(name, obj);
    }
    public void RemoveField(string name)
    {
        if (keys.IndexOf(name) > -1)
        {
            list.RemoveAt(keys.IndexOf(name));
            keys.Remove(name);
        }
    }
    public delegate void FieldNotFound(string name);
    public delegate void GetFieldResponse(JSONObject obj);
    public bool GetField(ref bool field, string name, bool fallback)
    {
        if (GetField(ref field, name)) { return true; }
        field = fallback;
        return false;
    }
    public bool GetField(ref bool field, string name, FieldNotFound fail = null)
    {
        if (type == Type.OBJECT)
        {
            int index = keys.IndexOf(name);
            if (index >= 0)
            {
                field = list[index].b;
                return true;
            }
        }
        if (fail != null) fail.Invoke(name);
        return false;
    }
#if USEFLOAT
    public bool GetField(ref float field, string name, float fallback)
    {
#else
	public bool GetField(ref double field, string name, double fallback) {
#endif
        if (GetField(ref field, name)) { return true; }
        field = fallback;
        return false;
    }
#if USEFLOAT
    public bool GetField(ref float field, string name, FieldNotFound fail = null)
    {
#else
	public bool GetField(ref double field, string name, FieldNotFound fail = null) {
#endif
        if (type == Type.OBJECT)
        {
            int index = keys.IndexOf(name);
            if (index >= 0)
            {
                field = list[index].n;
                return true;
            }
        }
        if (fail != null) fail.Invoke(name);
        return false;
    }
    public bool GetField(ref int field, string name, int fallback)
    {
        if (GetField(ref field, name)) { return true; }
        field = fallback;
        return false;
    }
    public bool GetField(ref int field, string name, FieldNotFound fail = null)
    {
        if (type == JSONObject.Type.OBJECT)
        {
            int index = keys.IndexOf(name);
            if (index >= 0)
            {
                field = (int)list[index].n;
                return true;
            }
        }
        if (fail != null) fail.Invoke(name);
        return false;
    }
    public bool GetField(ref uint field, string name, uint fallback)
    {
        if (GetField(ref field, name)) { return true; }
        field = fallback;
        return false;
    }
    public bool GetField(ref uint field, string name, FieldNotFound fail = null)
    {
        if (type == JSONObject.Type.OBJECT)
        {
            int index = keys.IndexOf(name);
            if (index >= 0)
            {
                field = (uint)list[index].n;
                return true;
            }
        }
        if (fail != null) fail.Invoke(name);
        return false;
    }
    public bool GetField(ref string field, string name, string fallback)
    {
        if (GetField(ref field, name)) { return true; }
        field = fallback;
        return false;
    }
    public bool GetField(ref string field, string name, FieldNotFound fail = null)
    {
        if (type == JSONObject.Type.OBJECT)
        {
            int index = keys.IndexOf(name);
            if (index >= 0)
            {
                field = list[index].str;
                return true;
            }
        }
        if (fail != null) fail.Invoke(name);
        return false;
    }
    public void GetField(string name, GetFieldResponse response, FieldNotFound fail = null)
    {
        if (response != null && type == Type.OBJECT)
        {
            int index = keys.IndexOf(name);
            if (index >= 0)
            {
                response.Invoke(list[index]);
                return;
            }
        }
        if (fail != null) fail.Invoke(name);
    }
    public JSONObject GetField(string name)
    {
        if (type == JSONObject.Type.OBJECT)
            for (int i = 0; i < keys.Count; i++)
                if ((string)keys[i] == name)
                    return (JSONObject)list[i];
        return null;
    }
    public bool HasFields(string[] names)
    {
        foreach (string name in names)
            if (!keys.Contains(name))
                return false;
        return true;
    }
    public bool HasField(string name)
    {
        if (type == JSONObject.Type.OBJECT)
            for (int i = 0; i < keys.Count; i++)
                if ((string)keys[i] == name)
                    return true;
        return false;
    }
    public void Clear()
    {
        type = JSONObject.Type.NULL;
        if (list != null)
            list.Clear();
        if (keys != null)
            keys.Clear();
        str = "";
        n = 0;
        b = false;
    }
    public JSONObject Copy()
    {
        return new JSONObject(print());
    }
    /*
     * The Merge function is experimental. Use at your own risk.
     */
    public void Merge(JSONObject obj)
    {
        MergeRecur(this, obj);
    }
    /// <summary>
    /// Merge object right into left recursively
    /// </summary>
    /// <param name="left">The left (base) object</param>
    /// <param name="right">The right (new) object</param>
    static void MergeRecur(JSONObject left, JSONObject right)
    {
        if (left.type == JSONObject.Type.NULL)
            left.Absorb(right);
        else if (left.type == Type.OBJECT && right.type == Type.OBJECT)
        {
            for (int i = 0; i < right.list.Count; i++)
            {
                string key = (string)right.keys[i];
                if (right[i].isContainer)
                {
                    if (left.HasField(key))
                        MergeRecur(left[key], right[i]);
                    else
                        left.AddField(key, right[i]);
                }
                else
                {
                    if (left.HasField(key))
                        left.SetField(key, right[i]);
                    else
                        left.AddField(key, right[i]);
                }
            }
        }
        else if (left.type == Type.ARRAY && right.type == Type.ARRAY)
        {
            if (right.Count > left.Count)
            {
                Debug.LogError("Cannot merge arrays when right object has more elements");
                return;
            }
            for (int i = 0; i < right.list.Count; i++)
            {
                if (left[i].type == right[i].type)
                {			//Only overwrite with the same type
                    if (left[i].isContainer)
                        MergeRecur(left[i], right[i]);
                    else
                    {
                        left[i] = right[i];
                    }
                }
            }
        }
    }
    public string print(bool pretty = false)
    {
        return print(0, pretty);
    }
    #region STRINGIFY
    public string print(int depth, bool pretty = false)
    {	//Convert the JSONObject into a string
        if (depth++ > MAX_DEPTH)
        {
            Debug.Log("reached max depth!");
            return "";
        }
        string str = "";
        switch (type)
        {
            case Type.STRING:
                str = "\"" + this.str + "\"";
                break;
            case Type.NUMBER:
#if USEFLOAT
                if (float.IsInfinity(n))
                    str = INFINITY;
                else if (float.IsNegativeInfinity(n))
                    str = NEGINFINITY;
                else if (float.IsNaN(n))
                    str = NaN;
#else
				if(double.IsInfinity(n))
					str = INFINITY;
				else if(double.IsNegativeInfinity(n))
					str = NEGINFINITY;
				else if(double.IsNaN(n))
					str = NaN;
#endif
                else
                    str += n;
                break;

            case JSONObject.Type.OBJECT:
                str = "{";
                if (list.Count > 0)
                {
#if(PRETTY)	//for a bit more readability, comment the define above to disable system-wide
                    if (pretty)
                        str += "\n";
#endif
                    for (int i = 0; i < list.Count; i++)
                    {
                        string key = (string)keys[i];
                        JSONObject obj = (JSONObject)list[i];
                        if (obj)
                        {
#if(PRETTY)
                            if (pretty)
                                for (int j = 0; j < depth; j++)
                                    str += "\t"; //for a bit more readability
#endif
                            str += "\"" + key + "\":";
                            str += obj.print(depth, pretty) + ",";
#if(PRETTY)
                            if (pretty)
                                str += "\n";
#endif
                        }
                    }
#if(PRETTY)
                    if (pretty)
                        str = str.Substring(0, str.Length - 1);		//BOP: This line shows up twice on purpose: once to remove the \n if readable is true and once to remove the comma
#endif
                    str = str.Substring(0, str.Length - 1);
                }
#if(PRETTY)
                if (pretty && list.Count > 0)
                {
                    str += "\n";
                    for (int j = 0; j < depth - 1; j++)
                        str += "\t"; //for a bit more readability
                }
#endif
                str += "}";
                break;
            case JSONObject.Type.ARRAY:
                str = "[";
                if (list.Count > 0)
                {
#if(PRETTY)
                    if (pretty)
                        str += "\n"; //for a bit more readability
#endif
                    foreach (JSONObject obj in list)
                    {
                        if (obj)
                        {
#if(PRETTY)
                            if (pretty)
                                for (int j = 0; j < depth; j++)
                                    str += "\t"; //for a bit more readability
#endif
                            str += obj.print(depth, pretty) + ",";
#if(PRETTY)
                            if (pretty)
                                str += "\n"; //for a bit more readability
#endif
                        }
                    }
#if(PRETTY)
                    if (pretty)
                        str = str.Substring(0, str.Length - 1);	//BOP: This line shows up twice on purpose: once to remove the \n if readable is true and once to remove the comma
#endif
                    str = str.Substring(0, str.Length - 1);
                }
#if(PRETTY)
                if (pretty && list.Count > 0)
                {
                    str += "\n";
                    for (int j = 0; j < depth - 1; j++)
                        str += "\t"; //for a bit more readability
                }
#endif
                str += "]";
                break;
            case Type.BOOL:
                if (b)
                    str = "true";
                else
                    str = "false";
                break;
            case Type.NULL:
                str = "null";
                break;
        }
        return str;
    }
    #endregion
    public static implicit operator WWWForm(JSONObject obj)
    {
        WWWForm form = new WWWForm();
        for (int i = 0; i < obj.list.Count; i++)
        {
            string key = i + "";
            if (obj.type == Type.OBJECT)
                key = obj.keys[i];
            string val = obj.list[i].ToString();
            if (obj.list[i].type == Type.STRING)
                val = val.Replace("\"", "");
            form.AddField(key, val);
        }
        return form;
    }
    public JSONObject this[int index]
    {
        get
        {
            if (list.Count > index) return (JSONObject)list[index];
            else return null;
        }
        set
        {
            if (list.Count > index)
                list[index] = value;
        }
    }
    public JSONObject this[string index]
    {
        get
        {
            return GetField(index);
        }
        set
        {
            SetField(index, value);
        }
    }
    public override string ToString()
    {
        return print();
    }
    public string ToString(bool pretty)
    {
        return print(pretty);
    }
    public Dictionary<string, string> ToDictionary()
    {
        if (type == Type.OBJECT)
        {
            Dictionary<string, string> result = new Dictionary<string, string>();
            for (int i = 0; i < list.Count; i++)
            {
                JSONObject val = (JSONObject)list[i];
                switch (val.type)
                {
                    case Type.STRING: result.Add((string)keys[i], val.str); break;
                    case Type.NUMBER: result.Add((string)keys[i], val.n + ""); break;
                    case Type.BOOL: result.Add((string)keys[i], val.b + ""); break;
                    default: Debug.LogWarning("Omitting object: " + (string)keys[i] + " in dictionary conversion"); break;
                }
            }
            return result;
        }
        else Debug.LogWarning("Tried to turn non-Object JSONObject into a dictionary");
        return null;
    }
    public static implicit operator bool(JSONObject o)
    {
        return (object)o != null;
    }
}