    const std = @import("std");
    const debug = std.debug;
    const ArenaAllocator = std.heap.ArenaAllocator;
    const ArrayList = std.ArrayList;
    const StringArrayHashMap = std.StringArrayHashMap;
    const Allocator = std.mem.Allocator;

    const StringifyOptions = @import("./stringify.zig").StringifyOptions;
    const stringify = @import("./stringify.zig").stringify;

    const JsonScanner = @import("./scanner.zig").Scanner;
    const AllocWhen = @import("./scanner.zig").AllocWhen;
    const Token = @import("./scanner.zig").Token;
    const isNumberFormattedLikeAnInteger = @import("./scanner.zig").isNumberFormattedLikeAnInteger;

    pub const ValueTree = struct {
        arena: *ArenaAllocator,
        root: Value,

        pub fn deinit(self: *ValueTree) void {
            self.arena.deinit();
            self.arena.child_allocator.destroy(self.arena);
        }
    };

    pub const ObjectMap = StringArrayHashMap(Value);
    pub const Array = ArrayList(Value);

    /// Represents a JSON value
    /// Currently only supports numbers that fit into i64 or f64.
    pub const Value = union(enum) {
        null,
        bool: bool,
        integer: i64,
        float: f64,
        number_string: []const u8,
        string: []const u8,
        array: Array,
        object: ObjectMap,

        pub fn jsonStringify(
            value: @This(),
            options: StringifyOptions,
            out_stream: anytype,
        ) @TypeOf(out_stream).Error!void {
            switch (value) {
                .null => try stringify(null, options, out_stream),
                .bool => |inner| try stringify(inner, options, out_stream),
                .integer => |inner| try stringify(inner, options, out_stream),
                .float => |inner| try stringify(inner, options, out_stream),
                .number_string => |inner| try out_stream.writeAll(inner),
                .string => |inner| try stringify(inner, options, out_stream),
                .array => |inner| try stringify(inner.items, options, out_stream),
                .object => |inner| {
                    try out_stream.writeByte('{');
                    var field_output = false;
                    var child_options = options;
                    child_options.whitespace.indent_level += 1;
                    var it = inner.iterator();
                    while (it.next()) |entry| {
                        if (!field_output) {
                            field_output = true;
                        } else {
                            try out_stream.writeByte(',');
                        }
                        try child_options.whitespace.outputIndent(out_stream);

                        try stringify(entry.key_ptr.*, options, out_stream);
                        try out_stream.writeByte(':');
                        if (child_options.whitespace.separator) {
                            try out_stream.writeByte(' ');
                        }
                        try stringify(entry.value_ptr.*, child_options, out_stream);
                    }
                    if (field_output) {
                        try options.whitespace.outputIndent(out_stream);
                    }
                    try out_stream.writeByte('}');
                },
            }
        }

        pub fn dump(self: Value) void {
            std.debug.getStderrMutex().lock();
            defer std.debug.getStderrMutex().unlock();

            const stderr = std.io.getStdErr().writer();
            stringify(self, .{}, stderr) catch return;
        }
    };

    /// A non-stream JSON parser which constructs a tree of Value's.
    pub const Parser = struct {
        allocator: Allocator,
        state: State,
        alloc_when: AllocWhen,
        // Stores parent nodes and un-combined Values.

        stack: Array,

        const State = enum {
            object_key,
            object_value,
            array_value,
            simple,
        };

        pub fn init(allocator: Allocator, alloc_when: AllocWhen) Parser {
            return Parser{
                .allocator = allocator,
                .state = .simple,
                .alloc_when = alloc_when,
                .stack = Array.init(allocator),
            };
        }

        pub fn deinit(p: *Parser) void {
            p.stack.deinit();
        }

        pub fn reset(p: *Parser) void {
            p.state = .simple;
            p.stack.shrinkRetainingCapacity(0);
        }

        pub fn parse(p: *Parser, input: []const u8) !ValueTree {
            var scanner = JsonScanner.initCompleteInput(p.allocator, input);
            defer scanner.deinit();

            var arena = try p.allocator.create(ArenaAllocator);
            errdefer p.allocator.destroy(arena);

            arena.* = ArenaAllocator.init(p.allocator);
            errdefer arena.deinit();

            const allocator = arena.allocator();

            while (true) {
                const token = try scanner.nextAlloc(allocator, p.alloc_when);
                if (token == .end_of_document) break;
                try p.transition(allocator, token);
            }

            debug.assert(p.stack.items.len == 1);

            return ValueTree{
                .arena = arena,
                .root = p.stack.items[0],
            };
        }

        // Even though p.allocator exists, we take an explicit allocator so that allocation state

        // can be cleaned up on error correctly during a `parse` on call.

        fn transition(p: *Parser, allocator: Allocator, token: Token) !void {
            switch (p.state) {
                .object_key => switch (token) {
                    .object_end => {
                        if (p.stack.items.len == 1) {
                            return;
                        }

                        var value = p.stack.pop();
                        try p.pushToParent(&value);
                    },
                    .string => |s| {
                        try p.stack.append(Value{ .string = s });
                        p.state = .object_value;
                    },
                    .allocated_string => |s| {
                        try p.stack.append(Value{ .string = s });
                        p.state = .object_value;
                    },
                    else => unreachable,
                },
                .object_value => {
                    var object = &p.stack.items[p.stack.items.len - 2].object;
                    var key = p.stack.items[p.stack.items.len - 1].string;

                    switch (token) {
                        .object_begin => {
                            try p.stack.append(Value{ .object = ObjectMap.init(allocator) });
                            p.state = .object_key;
                        },
                        .array_begin => {
                            try p.stack.append(Value{ .array = Array.init(allocator) });
                            p.state = .array_value;
                        },
                        .string => |s| {
                            try object.put(key, Value{ .string = s });
                            _ = p.stack.pop();
                            p.state = .object_key;
                        },
                        .allocated_string => |s| {
                            try object.put(key, Value{ .string = s });
                            _ = p.stack.pop();
                            p.state = .object_key;
                        },
                        .number => |slice| {
                            try object.put(key, try p.parseNumber(slice));
                            _ = p.stack.pop();
                            p.state = .object_key;
                        },
                        .allocated_number => |slice| {
                            try object.put(key, try p.parseNumber(slice));
                            _ = p.stack.pop();
                            p.state = .object_key;
                        },
                        .true => {
                            try object.put(key, Value{ .bool = true });
                            _ = p.stack.pop();
                            p.state = .object_key;
                        },
                        .false => {
                            try object.put(key, Value{ .bool = false });
                            _ = p.stack.pop();
                            p.state = .object_key;
                        },
                        .null => {
                            try object.put(key, .null);
                            _ = p.stack.pop();
                            p.state = .object_key;
                        },
                        .object_end, .array_end, .end_of_document => unreachable,
                        .partial_number, .partial_string, .partial_string_escaped_1, .partial_string_escaped_2, .partial_string_escaped_3, .partial_string_escaped_4 => unreachable,
                    }
                },
                .array_value => {
                    var array = &p.stack.items[p.stack.items.len - 1].array;

                    switch (token) {
                        .array_end => {
                            if (p.stack.items.len == 1) {
                                return;
                            }

                            var value = p.stack.pop();
                            try p.pushToParent(&value);
                        },
                        .object_begin => {
                            try p.stack.append(Value{ .object = ObjectMap.init(allocator) });
                            p.state = .object_key;
                        },
                        .array_begin => {
                            try p.stack.append(Value{ .array = Array.init(allocator) });
                            p.state = .array_value;
                        },
                        .string => |s| {
                            try array.append(Value{ .string = s });
                        },
                        .allocated_string => |s| {
                            try array.append(Value{ .string = s });
                        },
                        .number => |slice| {
                            try array.append(try p.parseNumber(slice));
                        },
                        .allocated_number => |slice| {
                            try array.append(try p.parseNumber(slice));
                        },
                        .true => {
                            try array.append(Value{ .bool = true });
                        },
                        .false => {
                            try array.append(Value{ .bool = false });
                        },
                        .null => {
                            try array.append(.null);
                        },
                        .object_end, .end_of_document => unreachable,
                        .partial_number, .partial_string, .partial_string_escaped_1, .partial_string_escaped_2, .partial_string_escaped_3, .partial_string_escaped_4 => unreachable,
                    }
                },
                .simple => switch (token) {
                    .object_begin => {
                        try p.stack.append(Value{ .object = ObjectMap.init(allocator) });
                        p.state = .object_key;
                    },
                    .array_begin => {
                        try p.stack.append(Value{ .array = Array.init(allocator) });
                        p.state = .array_value;
                    },
                    .string => |s| {
                        try p.stack.append(Value{ .string = s });
                    },
                    .allocated_string => |s| {
                        try p.stack.append(Value{ .string = s });
                    },
                    .number => |slice| {
                        try p.stack.append(try p.parseNumber(slice));
                    },
                    .allocated_number => |slice| {
                        try p.stack.append(try p.parseNumber(slice));
                    },
                    .true => {
                        try p.stack.append(Value{ .bool = true });
                    },
                    .false => {
                        try p.stack.append(Value{ .bool = false });
                    },
                    .null => {
                        try p.stack.append(.null);
                    },
                    .object_end, .array_end, .end_of_document => unreachable,
                    .partial_number, .partial_string, .partial_string_escaped_1, .partial_string_escaped_2, .partial_string_escaped_3, .partial_string_escaped_4 => unreachable,
                },
            }
        }

        fn pushToParent(p: *Parser, value: *const Value) !void {
            switch (p.stack.items[p.stack.items.len - 1]) {
                // Object Parent -> [ ..., object, <key>, value ]

                .string => |key| {
                    _ = p.stack.pop();

                    var object = &p.stack.items[p.stack.items.len - 1].object;
                    try object.put(key, value.*);
                    p.state = .object_key;
                },
                // Array Parent -> [ ..., <array>, value ]

                .array => |*array| {
                    try array.append(value.*);
                    p.state = .array_value;
                },
                else => {
                    unreachable;
                },
            }
        }

        fn parseNumber(p: *Parser, slice: []const u8) !Value {
            _ = p;
            return if (isNumberFormattedLikeAnInteger(slice))
                Value{
                    .integer = std.fmt.parseInt(i64, slice, 10) catch |e| switch (e) {
                        error.Overflow => return Value{ .number_string = slice },
                        error.InvalidCharacter => |err| return err,
                    },
                }
            else
                Value{ .float = try std.fmt.parseFloat(f64, slice) };
        }
    };

    test {
        _ = @import("dynamic_test.zig");
    }