using System;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
namespace InteractiveVideo
{
///
/// Minimal JSON reader (RFC 8259) with no dependencies. Exists because JsonUtility cannot read nested
/// arrays such as "polygon": [[x, y], ...]. Produces plain objects:
/// object -> Dictionary<string, object>, array -> List<object>, number -> double,
/// string -> string, true/false -> bool, null -> null.
/// Parsing happens once at load time, so clarity beats speed here.
///
public static class LightweightJson
{
public static object Parse(string json)
{
if (json == null) throw new FormatException("JSON text is null.");
var p = new Parser(json);
p.SkipWhitespace();
object value = p.ReadValue();
p.SkipWhitespace();
if (!p.AtEnd) throw p.Error("Unexpected trailing content");
return value;
}
// ---- typed accessors used by the loader ----
public static Dictionary AsObject(object o) => o as Dictionary;
public static List AsArray(object o) => o as List;
public static bool TryGet(Dictionary obj, string key, out object value)
{
value = null;
return obj != null && obj.TryGetValue(key, out value) && value != null;
}
public static string GetString(Dictionary obj, string key, string fallback = null) =>
TryGet(obj, key, out var v) ? (v is string s ? s : Convert.ToString(v, CultureInfo.InvariantCulture)) : fallback;
public static double GetNumber(Dictionary obj, string key, double fallback = 0) =>
TryGet(obj, key, out var v) && v is double d ? d : fallback;
public static bool GetBool(Dictionary obj, string key, bool fallback = false) =>
TryGet(obj, key, out var v) && v is bool b ? b : fallback;
public static double ToNumber(object o, double fallback = 0) => o is double d ? d : fallback;
private struct Parser
{
private readonly string _s;
private int _i;
public Parser(string s) { _s = s; _i = 0; }
public bool AtEnd => _i >= _s.Length;
public FormatException Error(string message) => new FormatException($"{message} at character {_i}.");
public void SkipWhitespace()
{
while (_i < _s.Length)
{
char c = _s[_i];
if (c == ' ' || c == '\t' || c == '\n' || c == '\r') _i++;
else break;
}
}
public object ReadValue()
{
if (AtEnd) throw Error("Unexpected end of JSON");
char c = _s[_i];
switch (c)
{
case '{': return ReadObject();
case '[': return ReadArray();
case '"': return ReadString();
case 't': ExpectLiteral("true"); return true;
case 'f': ExpectLiteral("false"); return false;
case 'n': ExpectLiteral("null"); return null;
default:
if (c == '-' || (c >= '0' && c <= '9')) return ReadNumber();
throw Error($"Unexpected character '{c}'");
}
}
private Dictionary ReadObject()
{
var result = new Dictionary();
_i++; // {
SkipWhitespace();
if (Peek() == '}') { _i++; return result; }
while (true)
{
SkipWhitespace();
if (Peek() != '"') throw Error("Expected string key");
string key = ReadString();
SkipWhitespace();
if (Peek() != ':') throw Error("Expected ':'");
_i++;
SkipWhitespace();
result[key] = ReadValue();
SkipWhitespace();
char c = Peek();
if (c == ',') { _i++; continue; }
if (c == '}') { _i++; return result; }
throw Error("Expected ',' or '}'");
}
}
private List ReadArray()
{
var result = new List();
_i++; // [
SkipWhitespace();
if (Peek() == ']') { _i++; return result; }
while (true)
{
SkipWhitespace();
result.Add(ReadValue());
SkipWhitespace();
char c = Peek();
if (c == ',') { _i++; continue; }
if (c == ']') { _i++; return result; }
throw Error("Expected ',' or ']'");
}
}
private string ReadString()
{
_i++; // opening quote
StringBuilder sb = null;
int start = _i;
while (true)
{
if (AtEnd) throw Error("Unterminated string");
char c = _s[_i];
if (c == '"')
{
string plain = _s.Substring(start, _i - start);
_i++;
return sb == null ? plain : sb.Append(plain).ToString();
}
if (c == '\\')
{
sb ??= new StringBuilder();
sb.Append(_s, start, _i - start);
_i++;
if (AtEnd) throw Error("Unterminated escape");
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 Error("Bad unicode escape");
sb.Append((char)int.Parse(_s.Substring(_i, 4), NumberStyles.HexNumber, CultureInfo.InvariantCulture));
_i += 4;
break;
default: throw Error($"Bad escape '\\{e}'");
}
start = _i;
continue;
}
_i++;
}
}
private object ReadNumber()
{
int start = _i;
if (Peek() == '-') _i++;
while (!AtEnd)
{
char c = _s[_i];
if ((c >= '0' && c <= '9') || c == '.' || c == 'e' || c == 'E' || c == '+' || c == '-') _i++;
else break;
}
string text = _s.Substring(start, _i - start);
if (!double.TryParse(text, NumberStyles.Float, CultureInfo.InvariantCulture, out double value))
throw Error($"Invalid number '{text}'");
return value;
}
private void ExpectLiteral(string literal)
{
if (string.CompareOrdinal(_s, _i, literal, 0, literal.Length) != 0) throw Error($"Expected '{literal}'");
_i += literal.Length;
}
private char Peek() => AtEnd ? '\0' : _s[_i];
}
}
}