001package gudusoft.gsqlparser.pp.score.json;
002
003import java.util.Collection;
004import java.util.Iterator;
005import java.util.Locale;
006import java.util.Map;
007
008/**
009 * Minimal, dependency-free JSON writer with deterministic output.
010 *
011 * <p>Handles {@link Map} (insertion order preserved), {@link Collection},
012 * arrays, {@link String}, {@link Number}, {@link Boolean}, {@link Enum} (as
013 * its lower-case name) and {@code null}. Doubles are written with at most
014 * three decimals and no exponent so two runs over the same input produce
015 * byte-identical text.
016 *
017 * <p>Package-private on purpose: the JSON contract is owned by
018 * {@code ScoreReport}; nothing else should serialise ad hoc.
019 */
020public final class JsonOut {
021
022    private JsonOut() {
023    }
024
025    public static String toJson(Object value, boolean pretty) {
026        StringBuilder sb = new StringBuilder(1024);
027        write(sb, value, pretty ? 0 : -1);
028        return sb.toString();
029    }
030
031    /** Format a double for the wire: at most 3 decimals, no trailing zeros, no exponent. */
032    public static String num(double d) {
033        if (Double.isNaN(d) || Double.isInfinite(d)) return "null";
034        if (d == Math.rint(d) && Math.abs(d) < 1e15) {
035            return Long.toString((long) d);
036        }
037        String s = String.format(Locale.ROOT, "%.3f", d);
038        int end = s.length();
039        while (end > 0 && s.charAt(end - 1) == '0') end--;
040        if (end > 0 && s.charAt(end - 1) == '.') end--;
041        return s.substring(0, end);
042    }
043
044    private static void write(StringBuilder sb, Object v, int indent) {
045        if (v == null) {
046            sb.append("null");
047        } else if (v instanceof String) {
048            quote(sb, (String) v);
049        } else if (v instanceof Boolean) {
050            sb.append(((Boolean) v).booleanValue() ? "true" : "false");
051        } else if (v instanceof Double || v instanceof Float) {
052            sb.append(num(((Number) v).doubleValue()));
053        } else if (v instanceof Number) {
054            sb.append(v.toString());
055        } else if (v instanceof Enum) {
056            quote(sb, ((Enum<?>) v).name().toLowerCase(Locale.ROOT));
057        } else if (v instanceof Map) {
058            writeMap(sb, (Map<?, ?>) v, indent);
059        } else if (v instanceof Collection) {
060            writeArray(sb, ((Collection<?>) v).iterator(), ((Collection<?>) v).size(), indent);
061        } else if (v instanceof int[]) {
062            int[] a = (int[]) v;
063            sb.append('[');
064            for (int i = 0; i < a.length; i++) {
065                if (i > 0) sb.append(',');
066                sb.append(a[i]);
067            }
068            sb.append(']');
069        } else if (v instanceof Object[]) {
070            Object[] a = (Object[]) v;
071            writeArray(sb, java.util.Arrays.asList(a).iterator(), a.length, indent);
072        } else {
073            quote(sb, v.toString());
074        }
075    }
076
077    private static void writeMap(StringBuilder sb, Map<?, ?> m, int indent) {
078        if (m.isEmpty()) {
079            sb.append("{}");
080            return;
081        }
082        sb.append('{');
083        boolean first = true;
084        for (Map.Entry<?, ?> e : m.entrySet()) {
085            if (!first) sb.append(',');
086            first = false;
087            newline(sb, indent < 0 ? -1 : indent + 1);
088            quote(sb, String.valueOf(e.getKey()));
089            sb.append(indent >= 0 ? ": " : ":");
090            write(sb, e.getValue(), indent >= 0 ? indent + 1 : -1);
091        }
092        newline(sb, indent);
093        sb.append('}');
094    }
095
096    private static void writeArray(StringBuilder sb, Iterator<?> it, int size, int indent) {
097        if (size == 0) {
098            sb.append("[]");
099            return;
100        }
101        sb.append('[');
102        boolean first = true;
103        while (it.hasNext()) {
104            if (!first) sb.append(',');
105            first = false;
106            newline(sb, indent < 0 ? -1 : indent + 1);
107            write(sb, it.next(), indent >= 0 ? indent + 1 : -1);
108        }
109        newline(sb, indent);
110        sb.append(']');
111    }
112
113    private static void newline(StringBuilder sb, int indent) {
114        if (indent < 0) return;
115        sb.append('\n');
116        for (int i = 0; i < indent; i++) sb.append("  ");
117    }
118
119    public static void quote(StringBuilder sb, String s) {
120        sb.append('"');
121        for (int i = 0; i < s.length(); i++) {
122            char c = s.charAt(i);
123            switch (c) {
124                case '"': sb.append("\\\""); break;
125                case '\\': sb.append("\\\\"); break;
126                case '\n': sb.append("\\n"); break;
127                case '\r': sb.append("\\r"); break;
128                case '\t': sb.append("\\t"); break;
129                case '\b': sb.append("\\b"); break;
130                case '\f': sb.append("\\f"); break;
131                default:
132                    if (c < 0x20) {
133                        sb.append(String.format(Locale.ROOT, "\\u%04x", (int) c));
134                    } else {
135                        sb.append(c);
136                    }
137            }
138        }
139        sb.append('"');
140    }
141}