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

namespace ServiceSim
{
    /// <summary>
    /// 小さな JSON の読み取り。サイトが書き出した店の形の JSON を読むためだけに使う。
    /// Unity の JsonUtility は入れ子の配列（[[x, y], [x, y]]）や、キーの決まっていない
    /// 入れ物を読めないので、ここに最小限のものを置く。書き出しはしない。
    ///
    /// 読めるもの: オブジェクト / 配列 / 文字列 / 数 / true / false / null
    /// 読めないもの: コメント、末尾のカンマ（JSON の決まりどおりのものだけ読む）
    /// </summary>
    public class JsonValue
    {
        public enum Kind { Null, Bool, Number, String, Array, Object }

        public Kind kind = Kind.Null;
        public bool boolValue;
        public double number;
        public string text;
        public List<JsonValue> items;
        public Dictionary<string, JsonValue> members;

        static readonly JsonValue NullValue = new JsonValue();

        public bool IsNull { get { return kind == Kind.Null; } }
        public bool IsArray { get { return kind == Kind.Array; } }
        public bool IsObject { get { return kind == Kind.Object; } }

        /// <summary>配列の長さ（配列でなければ 0）</summary>
        public int Count { get { return kind == Kind.Array && items != null ? items.Count : 0; } }

        /// <summary>配列の i 番目。無ければ null 値（例外を投げない）</summary>
        public JsonValue this[int i]
        {
            get
            {
                if (kind != Kind.Array || items == null || i < 0 || i >= items.Count) return NullValue;
                return items[i];
            }
        }

        /// <summary>オブジェクトの key。無ければ null 値（例外を投げない）</summary>
        public JsonValue this[string key]
        {
            get
            {
                JsonValue v;
                if (kind == Kind.Object && members != null && members.TryGetValue(key, out v)) return v;
                return NullValue;
            }
        }

        public bool Has(string key)
        {
            return kind == Kind.Object && members != null && members.ContainsKey(key);
        }

        public IEnumerable<string> Keys
        {
            get
            {
                if (kind == Kind.Object && members != null)
                    foreach (string k in members.Keys) yield return k;
            }
        }

        public float F(float fallback = 0f)
        {
            return kind == Kind.Number ? (float)number : fallback;
        }

        public int I(int fallback = 0)
        {
            return kind == Kind.Number ? (int)Math.Round(number) : fallback;
        }

        public string S(string fallback = null)
        {
            return kind == Kind.String ? text : fallback;
        }

        public bool B(bool fallback = false)
        {
            if (kind == Kind.Bool) return boolValue;
            if (kind == Kind.Number) return number != 0.0;
            return fallback;
        }

        // ---- 読み取り ----

        /// <summary>読む。読めなければ error に理由を入れて null を返す</summary>
        public static JsonValue Parse(string src, out string error)
        {
            error = null;
            if (string.IsNullOrEmpty(src)) { error = "中身が空です"; return null; }
            int i = 0;
            try
            {
                JsonValue v = ParseValue(src, ref i);
                SkipWhite(src, ref i);
                if (i < src.Length)
                {
                    error = "終わりのあとに文字が残っています（位置 " + i + "）";
                    return null;
                }
                return v;
            }
            catch (Exception e)
            {
                error = e.Message;
                return null;
            }
        }

        static void SkipWhite(string s, ref int i)
        {
            while (i < s.Length)
            {
                char c = s[i];
                if (c == ' ' || c == '\t' || c == '\r' || c == '\n') i++;
                else break;
            }
        }

        static JsonValue ParseValue(string s, ref int i)
        {
            SkipWhite(s, ref i);
            if (i >= s.Length) throw new Exception("途中で終わっています");
            char c = s[i];
            switch (c)
            {
                case '{': return ParseObject(s, ref i);
                case '[': return ParseArray(s, ref i);
                case '"': return new JsonValue { kind = Kind.String, text = ParseString(s, ref i) };
                case 't': Expect(s, ref i, "true"); return new JsonValue { kind = Kind.Bool, boolValue = true };
                case 'f': Expect(s, ref i, "false"); return new JsonValue { kind = Kind.Bool, boolValue = false };
                case 'n': Expect(s, ref i, "null"); return new JsonValue();
                default: return ParseNumber(s, ref i);
            }
        }

        static void Expect(string s, ref int i, string word)
        {
            if (i + word.Length > s.Length || string.CompareOrdinal(s, i, word, 0, word.Length) != 0)
                throw new Exception("読めない語があります（位置 " + i + "）");
            i += word.Length;
        }

        static JsonValue ParseObject(string s, ref int i)
        {
            JsonValue v = new JsonValue { kind = Kind.Object, members = new Dictionary<string, JsonValue>() };
            i++;                                   // '{'
            SkipWhite(s, ref i);
            if (i < s.Length && s[i] == '}') { i++; return v; }
            while (true)
            {
                SkipWhite(s, ref i);
                if (i >= s.Length || s[i] != '"') throw new Exception("キーが文字列ではありません（位置 " + i + "）");
                string key = ParseString(s, ref i);
                SkipWhite(s, ref i);
                if (i >= s.Length || s[i] != ':') throw new Exception("コロンがありません（位置 " + i + "）");
                i++;
                v.members[key] = ParseValue(s, ref i);
                SkipWhite(s, ref i);
                if (i >= s.Length) throw new Exception("オブジェクトが閉じていません");
                if (s[i] == ',') { i++; continue; }
                if (s[i] == '}') { i++; return v; }
                throw new Exception("オブジェクトの区切りが読めません（位置 " + i + "）");
            }
        }

        static JsonValue ParseArray(string s, ref int i)
        {
            JsonValue v = new JsonValue { kind = Kind.Array, items = new List<JsonValue>() };
            i++;                                   // '['
            SkipWhite(s, ref i);
            if (i < s.Length && s[i] == ']') { i++; return v; }
            while (true)
            {
                v.items.Add(ParseValue(s, ref i));
                SkipWhite(s, ref i);
                if (i >= s.Length) throw new Exception("配列が閉じていません");
                if (s[i] == ',') { i++; continue; }
                if (s[i] == ']') { i++; return v; }
                throw new Exception("配列の区切りが読めません（位置 " + i + "）");
            }
        }

        static string ParseString(string s, ref int i)
        {
            i++;                                   // '"'
            StringBuilder sb = new StringBuilder();
            while (i < s.Length)
            {
                char c = s[i++];
                if (c == '"') return sb.ToString();
                if (c != '\\') { sb.Append(c); continue; }
                if (i >= s.Length) break;
                char e = s[i++];
                switch (e)
                {
                    case '"': sb.Append('"'); break;
                    case '\\': sb.Append('\\'); break;
                    case '/': sb.Append('/'); break;
                    case 'b': sb.Append('\b'); break;
                    case 'f': sb.Append('\f'); break;
                    case 'n': sb.Append('\n'); break;
                    case 'r': sb.Append('\r'); break;
                    case 't': sb.Append('\t'); break;
                    case 'u':
                        if (i + 4 > s.Length) throw new Exception("\\u が短すぎます（位置 " + i + "）");
                        sb.Append((char)Convert.ToInt32(s.Substring(i, 4), 16));
                        i += 4;
                        break;
                    default: throw new Exception("読めない逃げ文字 \\" + e + "（位置 " + i + "）");
                }
            }
            throw new Exception("文字列が閉じていません");
        }

        static JsonValue ParseNumber(string s, ref int i)
        {
            int start = i;
            if (i < s.Length && (s[i] == '-' || s[i] == '+')) i++;
            while (i < s.Length)
            {
                char c = s[i];
                if ((c >= '0' && c <= '9') || c == '.' || c == 'e' || c == 'E' || c == '+' || c == '-') i++;
                else break;
            }
            string t = s.Substring(start, i - start);
            double d;
            if (!double.TryParse(t, NumberStyles.Float, CultureInfo.InvariantCulture, out d))
                throw new Exception("数として読めません: " + t + "（位置 " + start + "）");
            return new JsonValue { kind = Kind.Number, number = d };
        }
    }
}
