/*
 * Json.java — hand-rolled strict JSON parser for the AER-1 verifier port.
 *
 * Semantics mirror JS JSON.parse as closely as Java allows:
 *   - objects  -> java.util.LinkedHashMap<String,Object>  (insertion order;
 *                duplicate keys keep the FIRST position and the LAST value,
 *                exactly like a JS object literal produced by JSON.parse)
 *   - arrays   -> java.util.ArrayList<Object>
 *   - strings  -> java.lang.String (UTF-16; \\uXXXX escapes may produce lone
 *                surrogate code units, same as JSON.parse)
 *   - numbers  -> Json.Num, which keeps BOTH the raw source lexeme and the
 *                double value. The raw lexeme matters: draft -10 requires
 *                rejecting a lexical float where an integer is required
 *                (e.g. "entry_count": 5.0), while "seq": 2.0 is accepted
 *                because JS Number.isInteger(2.0) is true.
 *   - booleans -> java.lang.Boolean
 *   - null     -> Json.NULL (a sentinel; Java null means "member absent")
 *
 * Grammar is RFC 8259 strict: whitespace is space/tab/LF/CR only, no
 * comments, no trailing commas, no single quotes, no leading '+', no
 * leading zeros, no NaN/Infinity, unescaped control characters (< U+0020)
 * inside strings are rejected, and any trailing content is an error.
 */
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

public final class Json {

    private Json() {}

    /** Sentinel for JSON null (distinct from Java null = "absent member"). */
    public static final Object NULL = new Object() {
        @Override public String toString() { return "null"; }
    };

    /** A JSON number: raw lexeme + numeric value. */
    public static final class Num {
        public final String raw;
        public final double value;

        public Num(String raw) {
            this.raw = raw;
            this.value = Double.parseDouble(raw);
        }

        /** Construct from a computed value (for synthesized numbers). */
        public static Num of(double value) {
            return new Num(Aer1Verify.jsNumToString(value), value);
        }

        private Num(String raw, double value) {
            this.raw = raw;
            this.value = value;
        }

        /** Number.isInteger(v): integral finite double. */
        public boolean isInteger() {
            return !Double.isNaN(value) && !Double.isInfinite(value)
                   && value == Math.rint(value);
        }

        /*
         * Mirrors the kit runner's float marker: the -10 kit wraps any
         * /("entry_count"\s*:\s*)(\d+\.\d+)/ literal as {"__float__": N}
         * because JSON.parse erases the int/float lexical distinction.
         * Only unsigned digit strings containing a '.' with digits on both
         * sides are marked (a leading '-' or an exponent is NOT matched by
         * the kit's regex, so "-5.0" or "5e0" would slip through in JS too;
         * Num.isInteger() still applies on top).
         */
        public boolean isMarkedFloat() {
            return raw.matches("[0-9]+\\.[0-9]+");
        }

        @Override public String toString() { return raw; }
    }

    public static final class ParseException extends Exception {
        public final int pos;
        public ParseException(String msg, int pos) {
            super(msg + " (at offset " + pos + ")");
            this.pos = pos;
        }
    }

    public static Object parse(String text) throws ParseException {
        Parser p = new Parser(text);
        Object v = p.parseDocument();
        return v;
    }

    /* Convenience predicates used all over the verifier. */
    public static boolean isObj(Object v) { return v instanceof Map; }
    public static boolean isArr(Object v) { return v instanceof List; }
    public static boolean isStr(Object v) { return v instanceof String; }
    public static boolean isNum(Object v) { return v instanceof Num; }
    public static boolean isNull(Object v) { return v == NULL; }

    @SuppressWarnings("unchecked")
    public static Object get(Object obj, String key) {
        return ((Map<String, Object>) obj).get(key);
    }

    @SuppressWarnings("unchecked")
    public static boolean has(Object obj, String key) {
        return ((Map<String, Object>) obj).containsKey(key);
    }

    public static Object get(Object arr, int i) {
        return ((List<?>) arr).get(i);
    }

    public static int size(Object arr) {
        return ((List<?>) arr).size();
    }

    private static final class Parser {
        private final String s;
        private int i;
        private int depth;
        private static final int MAX_DEPTH = 512;

        Parser(String s) { this.s = s; this.i = 0; }

        private ParseException err(String msg) {
            return new ParseException(msg, i);
        }

        private boolean eof() { return i >= s.length(); }
        private char peek() { return s.charAt(i); }
        /* ASCII digits only — Character.isDigit also accepts Unicode digits. */
        private static boolean isAsciiDigit(char c) { return c >= '0' && c <= '9'; }

        private void skipWs() {
            while (!eof()) {
                char c = s.charAt(i);
                if (c == ' ' || c == '\t' || c == '\n' || c == '\r') i++;
                else break;
            }
        }

        Object parseDocument() throws ParseException {
            skipWs();
            if (eof()) throw err("empty input");
            Object v = parseValue();
            skipWs();
            if (!eof()) throw err("trailing characters after top-level value");
            return v;
        }

        private Object parseValue() throws ParseException {
            if (eof()) throw err("unexpected end of input");
            char c = peek();
            switch (c) {
                case '{': return parseObject();
                case '[': return parseArray();
                case '"': return parseString();
                case 't': return parseLiteral("true", Boolean.TRUE);
                case 'f': return parseLiteral("false", Boolean.FALSE);
                case 'n': return parseLiteral("null", NULL);
                default:
                    if (c == '-' || (c >= '0' && c <= '9')) return parseNumber();
                    throw err("unexpected character '" + c + "'");
            }
        }

        private Object parseLiteral(String word, Object value) throws ParseException {
            if (s.regionMatches(i, word, 0, word.length())) {
                i += word.length();
                return value;
            }
            throw err("invalid literal");
        }

        private Map<String, Object> parseObject() throws ParseException {
            if (++depth > MAX_DEPTH) throw err("nesting too deep");
            i++; // '{'
            LinkedHashMap<String, Object> map = new LinkedHashMap<>();
            skipWs();
            if (!eof() && peek() == '}') { i++; depth--; return map; }
            while (true) {
                skipWs();
                if (eof() || peek() != '"') throw err("expected object key string");
                String key = parseString();
                skipWs();
                if (eof() || peek() != ':') throw err("expected ':'");
                i++;
                skipWs();
                Object val = parseValue();
                map.put(key, val); // duplicate key: last value wins (JSON.parse parity)
                skipWs();
                if (eof()) throw err("unterminated object");
                char c = peek();
                if (c == ',') { i++; continue; }
                if (c == '}') { i++; depth--; return map; }
                throw err("expected ',' or '}'");
            }
        }

        private List<Object> parseArray() throws ParseException {
            if (++depth > MAX_DEPTH) throw err("nesting too deep");
            i++; // '['
            ArrayList<Object> list = new ArrayList<>();
            skipWs();
            if (!eof() && peek() == ']') { i++; depth--; return list; }
            while (true) {
                skipWs();
                list.add(parseValue());
                skipWs();
                if (eof()) throw err("unterminated array");
                char c = peek();
                if (c == ',') { i++; continue; }
                if (c == ']') { i++; depth--; return list; }
                throw err("expected ',' or ']'");
            }
        }

        private String parseString() throws ParseException {
            i++; // opening '"'
            StringBuilder sb = new StringBuilder();
            while (true) {
                if (eof()) throw err("unterminated string");
                char c = s.charAt(i++);
                if (c == '"') return sb.toString();
                if (c == '\\') {
                    if (eof()) throw err("unterminated escape");
                    char e = s.charAt(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 err("bad \\u escape");
                            int v = 0;
                            for (int k = 0; k < 4; k++) {
                                char h = s.charAt(i + k);
                                // ASCII hex only — JSON.parse parity.
                                int d;
                                if (h >= '0' && h <= '9') d = h - '0';
                                else if (h >= 'a' && h <= 'f') d = h - 'a' + 10;
                                else if (h >= 'A' && h <= 'F') d = h - 'A' + 10;
                                else throw err("bad \\u escape");
                                v = (v << 4) | d;
                            }
                            i += 4;
                            // Append the raw UTF-16 unit; a valid surrogate
                            // pair lands as two code units, a lone surrogate
                            // stays lone — identical to JSON.parse.
                            sb.append((char) v);
                            break;
                        }
                        default:
                            throw err("invalid escape '\\" + e + "'");
                    }
                } else {
                    if (c < 0x20) throw err("unescaped control character in string");
                    sb.append(c);
                }
            }
        }

        private Num parseNumber() throws ParseException {
            int start = i;
            if (!eof() && peek() == '-') i++;
            if (eof()) throw err("bad number");
            char c = peek();
            if (c == '0') {
                i++;
                if (!eof() && isAsciiDigit(peek()))
                    throw err("leading zeros are not allowed");
            } else if (c >= '1' && c <= '9') {
                while (!eof() && isAsciiDigit(peek())) i++;
            } else {
                throw err("bad number");
            }
            if (!eof() && peek() == '.') {
                i++;
                if (eof() || !isAsciiDigit(peek())) throw err("bad number");
                while (!eof() && isAsciiDigit(peek())) i++;
            }
            if (!eof() && (peek() == 'e' || peek() == 'E')) {
                i++;
                if (!eof() && (peek() == '+' || peek() == '-')) i++;
                if (eof() || !isAsciiDigit(peek())) throw err("bad number");
                while (!eof() && isAsciiDigit(peek())) i++;
            }
            String raw = s.substring(start, i);
            try {
                return new Num(raw);
            } catch (NumberFormatException ex) {
                // Should be unreachable given the grammar above.
                throw new ParseException("bad number '" + raw + "'", start);
            }
        }
    }
}
