namespace Json {

    function serialize(object, flags = Json.JsonSerializerFlag.DEFAULT, depth = 64) {
        builder = [];
        Json._serializeImpl(object, flags, depth, builder, (flags & Json.JsonSerializerFlag.PRETTY) != 0, 0);
        return builder.join('');
    }

    // TODO: move this down to translated code.
    function _serializeImpl(object, flags, depthRev, sb, isPretty, depth) {
        if (depthRev == 0) throw new Json.JsonException("Serialization depth exceed. Possible reference loop.");

        tab = JsonHelper.tabCache.get(depth) ?? JsonHelper.getTab(depth);

        switch (Core.typeof(object)) {
            case Core.Type.NULL:
                return sb.add('null');

            // The auto to-string for these types are good enough.
            case Core.Type.BOOLEAN:
            case Core.Type.INTEGER:
            case Core.Type.FLOAT:
                return sb.add('' + object);

            case Core.Type.STRING:
                sb.add('"');
                length = object.length;
                for (c : object) {
                    switch (c) {
                        case '"': sb.add('\\"'); break;
                        case '\\': sb.add('\\\\'); break;
                        // TODO: control characters
                        // TODO: unicode
                        default: sb.add(c); break;
                    }
                }
                return sb.add('"');

            case Core.Type.LIST:
                length = object.length;
                nextDepthRev = depthRev - 1;
                nextDepth = depth + 1;
                if (length == 0) return '[]';
                sb.add('[');
                if (isPretty) sb.add('\n');

                tab2 = isPretty ? (JsonHelper.tabCache.get(nextDepth) ?? JsonHelper.getTab(nextDepth)) : '';
                for (i = 0; i < length; ++i) {
                    if (isPretty) sb.add(tab2);
                    Json._serializeImpl(object[i], flags, nextDepthRev, sb, isPretty, nextDepth);
                    if (i < length - 1) sb.add(',');
                    if (isPretty) sb.add('\n');
                }
                if (isPretty) sb.add(tab);
                return sb.add(']');

            case Core.Type.DICTIONARY:
                omitNull = (flags & Json.JsonSerializerFlag.OMIT_DICTIONARY_NULLS) != 0;
                length = object.length;
                if (length == 0) return sb.add('{}');

                nextDepthRev = depthRev - 1;
                nextDepth = depth + 1;
                tab2 = isPretty ? (JsonHelper.tabCache.get(nextDepth) ?? JsonHelper.getTab(nextDepth)) : '';
                sb.add('{');
                if (isPretty) sb.add('\n');
                keys = object.keys();
                // This depends on dictionaries only containing one type.
                if (Core.typeof(keys[0]) != Core.Type.STRING) throw new Json.JsonException("Only string dictionaries can be used.");
                keys.sort(); // deterministic predictable output
                nextDepthRev = depthRev - 1;
                first = true;
                for (i = 0; i < length; ++i) {
                    key = keys[i];
                    value = object[key];
                    if (omitNull && value == null) {
                        continue;
                    }
                    if (isPretty) sb.add(tab2);
                    Json._serializeImpl(key, flags, 1, sb, isPretty, 1); // just a string, so depth doesn't matter
                    sb.add(isPretty ? ': ' : ':');
                    Json._serializeImpl(value, flags, nextDepthRev, sb, isPretty, nextDepth);
                    if (i < length - 1) {
                        sb.add(',');
                    }
                    if (isPretty) sb.add('\n');
                }
                if (isPretty) sb.add(tab);
                return sb.add('}');

            default:
                // TODO: say which type.
                throw new Json.JsonException("This type cannot be serialized to JSON.");
        }
    }
}
