const std = @import("std");
const Lexer = @import("lex.zig").Lexer;
const Token = @import("lex.zig").Token;
const Node = @import("ast.zig").Node;
const Tree = @import("ast.zig").Tree;
const CodePageLookup = @import("ast.zig").CodePageLookup;
const ResourceType = @import("rc.zig").ResourceType;
const Allocator = std.mem.Allocator;
const ErrorDetails = @import("errors.zig").ErrorDetails;
const ErrorDetailsWithoutCodePage = @import("errors.zig").ErrorDetailsWithoutCodePage;
const Diagnostics = @import("errors.zig").Diagnostics;
const SourceBytes = @import("literals.zig").SourceBytes;
const Compiler = @import("compile.zig").Compiler;
const rc = @import("rc.zig");
const res = @import("res.zig");

// TODO: Make these configurable?
pub const max_nested_menu_level: u32 = 512;
pub const max_nested_version_level: u32 = 512;
pub const max_nested_expression_level: u32 = 200;

pub const Parser = struct {
    const Self = @This();

    lexer: *Lexer,
    /// values that need to be initialized per-parse
    state: Parser.State = undefined,
    options: Parser.Options,

    pub const Error = error{ParseError} || Allocator.Error;

    pub const Options = struct {
        warn_instead_of_error_on_invalid_code_page: bool = false,
        disjoint_code_page: bool = false,
    };

    pub fn init(lexer: *Lexer, options: Options) Parser {
        return Parser{
            .lexer = lexer,
            .options = options,
        };
    }

    pub const State = struct {
        token: Token,
        lookahead_lexer: Lexer,
        allocator: Allocator,
        arena: Allocator,
        diagnostics: *Diagnostics,
        input_code_page_lookup: CodePageLookup,
        output_code_page_lookup: CodePageLookup,
        warned_about_disjoint_code_page: bool,
    };

    pub fn parse(self: *Self, allocator: Allocator, diagnostics: *Diagnostics) Error!*Tree {
        var arena = std.heap.ArenaAllocator.init(allocator);
        errdefer arena.deinit();

        self.state = Parser.State{
            .token = undefined,
            .lookahead_lexer = undefined,
            .allocator = allocator,
            .arena = arena.allocator(),
            .diagnostics = diagnostics,
            .input_code_page_lookup = CodePageLookup.init(arena.allocator(), self.lexer.default_code_page),
            .output_code_page_lookup = CodePageLookup.init(arena.allocator(), self.lexer.default_code_page),
            .warned_about_disjoint_code_page = false,
        };

        const parsed_root = try self.parseRoot();

        const tree = try self.state.arena.create(Tree);
        tree.* = .{
            .node = parsed_root,
            .input_code_pages = self.state.input_code_page_lookup,
            .output_code_pages = self.state.output_code_page_lookup,
            .source = self.lexer.buffer,
            .arena = arena.state,
            .allocator = allocator,
        };
        return tree;
    }

    fn parseRoot(self: *Self) Error!*Node {
        var statements: std.ArrayList(*Node) = .empty;
        defer statements.deinit(self.state.allocator);

        try self.parseStatements(&statements);
        try self.check(.eof);

        const node = try self.state.arena.create(Node.Root);
        node.* = .{
            .body = try self.state.arena.dupe(*Node, statements.items),
        };
        return &node.base;
    }

    fn parseStatements(self: *Self, statements: *std.ArrayList(*Node)) Error!void {
        while (true) {
            try self.nextToken(.whitespace_delimiter_only);
            if (self.state.token.id == .eof) break;
            // The Win32 compiler will sometimes try to recover from errors
            // and then restart parsing afterwards. We don't ever do this
            // because it almost always leads to unhelpful error messages
            // (usually it will end up with bogus things like 'file
            // not found: {')
            const statement = try self.parseStatement();
            try statements.append(self.state.allocator, statement);
        }
    }

    /// Expects the current token to be the token before possible common resource attributes.
    /// After return, the current token will be the token immediately before the end of the
    /// common resource attributes (if any). If there are no common resource attributes, the
    /// current token is unchanged.
    /// The returned slice is allocated by the parser's arena
    fn parseCommonResourceAttributes(self: *Self) ![]Token {
        var common_resource_attributes: std.ArrayList(Token) = .empty;
        while (true) {
            const maybe_common_resource_attribute = try self.lookaheadToken(.normal);
            if (maybe_common_resource_attribute.id == .literal and rc.CommonResourceAttributes.map.has(maybe_common_resource_attribute.slice(self.lexer.buffer))) {
                try common_resource_attributes.append(self.state.arena, maybe_common_resource_attribute);
                try self.nextToken(.normal);
            } else {
                break;
            }
        }
        return common_resource_attributes.toOwnedSlice(self.state.arena);
    }

    /// Expects the current token to have already been dealt with, and that the
    /// optional statements will potentially start on the next token.
    /// After return, the current token will be the token immediately before the end of the
    /// optional statements (if any). If there are no optional statements, the
    /// current token is unchanged.
    /// The returned slice is allocated by the parser's arena
    fn parseOptionalStatements(self: *Self, resource: ResourceType) ![]*Node {
        var optional_statements: std.ArrayList(*Node) = .empty;

        const num_statement_types = @typeInfo(rc.OptionalStatements).@"enum".field_names.len;
        var statement_type_has_duplicates: [num_statement_types]bool = @splat(false);
        var last_statement_per_type: [num_statement_types]?*Node = @splat(null);

        while (true) {
            const lookahead_token = try self.lookaheadToken(.normal);
            if (lookahead_token.id != .literal) break;
            const slice = lookahead_token.slice(self.lexer.buffer);
            const optional_statement_type = rc.OptionalStatements.map.get(slice) orelse switch (resource) {
                .dialog, .dialogex => rc.OptionalStatements.dialog_map.get(slice) orelse break,
                else => break,
            };
            try self.nextToken(.normal);

            const type_i = @intFromEnum(optional_statement_type);
            if (last_statement_per_type[type_i] != null) {
                statement_type_has_duplicates[type_i] = true;
            }

            switch (optional_statement_type) {
                .language => {
                    const language = try self.parseLanguageStatement();
                    try optional_statements.append(self.state.arena, language);
                },
                // Number only
                .version, .characteristics, .style, .exstyle => {
                    const identifier = self.state.token;
                    const value = try self.parseExpression(.{
                        .can_contain_not_expressions = optional_statement_type == .style or optional_statement_type == .exstyle,
                        .allowed_types = .{ .number = true },
                    });
                    const node = try self.state.arena.create(Node.SimpleStatement);
                    node.* = .{
                        .identifier = identifier,
                        .value = value,
                    };
                    try optional_statements.append(self.state.arena, &node.base);
                },
                // String only
                .caption => {
                    const identifier = self.state.token;
                    try self.nextToken(.normal);
                    const value = self.state.token;
                    if (!value.isStringLiteral()) {
                        return self.addErrorDetailsAndFail(.{
                            .err = .expected_something_else,
                            .token = value,
                            .extra = .{ .expected_types = .{
                                .string_literal = true,
                            } },
                        });
                    }
                    const value_node = try self.state.arena.create(Node.Literal);
                    value_node.* = .{
                        .token = value,
                    };
                    const node = try self.state.arena.create(Node.SimpleStatement);
                    node.* = .{
                        .identifier = identifier,
                        .value = &value_node.base,
                    };
                    try optional_statements.append(self.state.arena, &node.base);
                },
                // String or number
                .class => {
                    const identifier = self.state.token;
                    const value = try self.parseExpression(.{ .allowed_types = .{ .number = true, .string = true } });
                    const node = try self.state.arena.create(Node.SimpleStatement);
                    node.* = .{
                        .identifier = identifier,
                        .value = value,
                    };
                    try optional_statements.append(self.state.arena, &node.base);
                },
                // Special case
                .menu => {
                    const identifier = self.state.token;
                    try self.nextToken(.whitespace_delimiter_only);
                    try self.check(.literal);
                    const value_node = try self.state.arena.create(Node.Literal);
                    value_node.* = .{
                        .token = self.state.token,
                    };
                    const node = try self.state.arena.create(Node.SimpleStatement);
                    node.* = .{
                        .identifier = identifier,
                        .value = &value_node.base,
                    };
                    try optional_statements.append(self.state.arena, &node.base);
                },
                .font => {
                    const identifier = self.state.token;
                    const point_size = try self.parseExpression(.{ .allowed_types = .{ .number = true } });

                    // The comma between point_size and typeface is both optional and
                    // there can be any number of them
                    try self.skipAnyCommas();

                    try self.nextToken(.normal);
                    const typeface = self.state.token;
                    if (!typeface.isStringLiteral()) {
                        return self.addErrorDetailsAndFail(.{
                            .err = .expected_something_else,
                            .token = typeface,
                            .extra = .{ .expected_types = .{
                                .string_literal = true,
                            } },
                        });
                    }

                    const ExSpecificValues = struct {
                        weight: ?*Node = null,
                        italic: ?*Node = null,
                        char_set: ?*Node = null,
                    };
                    var ex_specific = ExSpecificValues{};
                    ex_specific: {
                        var optional_param_parser = OptionalParamParser{ .parser = self };
                        switch (resource) {
                            .dialogex => {
                                {
                                    ex_specific.weight = try optional_param_parser.parse(.{});
                                    if (optional_param_parser.finished) break :ex_specific;
                                }
                                {
                                    if (!(try self.parseOptionalToken(.comma))) break :ex_specific;
                                    ex_specific.italic = try self.parseExpression(.{ .allowed_types = .{ .number = true } });
                                }
                                {
                                    ex_specific.char_set = try optional_param_parser.parse(.{});
                                    if (optional_param_parser.finished) break :ex_specific;
                                }
                            },
                            .dialog => {},
                            else => unreachable, // only DIALOG and DIALOGEX have FONT optional-statements
                        }
                    }

                    const node = try self.state.arena.create(Node.FontStatement);
                    node.* = .{
                        .identifier = identifier,
                        .point_size = point_size,
                        .typeface = typeface,
                        .weight = ex_specific.weight,
                        .italic = ex_specific.italic,
                        .char_set = ex_specific.char_set,
                    };
                    try optional_statements.append(self.state.arena, &node.base);
                },
            }

            last_statement_per_type[type_i] = optional_statements.items[optional_statements.items.len - 1];
        }

        for (optional_statements.items) |optional_statement| {
            const type_i = type_i: {
                switch (optional_statement.id) {
                    .simple_statement => {
                        const simple_statement: *Node.SimpleStatement = @alignCast(@fieldParentPtr("base", optional_statement));
                        const statement_identifier = simple_statement.identifier;
                        const slice = statement_identifier.slice(self.lexer.buffer);
                        const optional_statement_type = rc.OptionalStatements.map.get(slice) orelse
                            rc.OptionalStatements.dialog_map.get(slice).?;
                        break :type_i @intFromEnum(optional_statement_type);
                    },
                    .font_statement => {
                        break :type_i @intFromEnum(rc.OptionalStatements.font);
                    },
                    .language_statement => {
                        break :type_i @intFromEnum(rc.OptionalStatements.language);
                    },
                    else => unreachable,
                }
            };
            if (!statement_type_has_duplicates[type_i]) continue;
            if (optional_statement == last_statement_per_type[type_i].?) continue;

            try self.addErrorDetails(.{
                .err = .duplicate_optional_statement_skipped,
                .type = .warning,
                .token = optional_statement.getFirstToken(),
                .token_span_start = optional_statement.getFirstToken(),
                .token_span_end = optional_statement.getLastToken(),
            });
        }

        return optional_statements.toOwnedSlice(self.state.arena);
    }

    /// Expects the current token to be the first token of the statement.
    fn parseStatement(self: *Self) Error!*Node {
        const first_token = self.state.token;
        std.debug.assert(first_token.id == .literal);

        if (rc.TopLevelKeywords.map.get(first_token.slice(self.lexer.buffer))) |keyword| switch (keyword) {
            .language => {
                const language_statement = try self.parseLanguageStatement();
                return language_statement;
            },
            .version, .characteristics => {
                const identifier = self.state.token;
                const value = try self.parseExpression(.{ .allowed_types = .{ .number = true } });
                const node = try self.state.arena.create(Node.SimpleStatement);
                node.* = .{
                    .identifier = identifier,
                    .value = value,
                };
                return &node.base;
            },
            .stringtable => {
                // common resource attributes must all be contiguous and come before optional-statements
                const common_resource_attributes = try self.parseCommonResourceAttributes();
                const optional_statements = try self.parseOptionalStatements(.stringtable);

                try self.nextToken(.normal);
                const begin_token = self.state.token;
                try self.check(.begin);

                var strings: std.ArrayList(*Node) = .empty;
                defer strings.deinit(self.state.allocator);
                while (true) {
                    const maybe_end_token = try self.lookaheadToken(.normal);
                    switch (maybe_end_token.id) {
                        .end => {
                            try self.nextToken(.normal);
                            break;
                        },
                        .eof => {
                            return self.addErrorDetailsWithCodePageAndFail(.{
                                .err = .unfinished_string_table_block,
                                .code_page = self.lexer.current_code_page,
                                .token = maybe_end_token,
                            });
                        },
                        else => {},
                    }
                    const id_expression = try self.parseExpression(.{ .allowed_types = .{ .number = true } });

                    const comma_token: ?Token = if (try self.parseOptionalToken(.comma)) self.state.token else null;

                    try self.nextToken(.normal);
                    if (self.state.token.id != .quoted_ascii_string and self.state.token.id != .quoted_wide_string) {
                        return self.addErrorDetailsAndFail(.{
                            .err = .expected_something_else,
                            .token = self.state.token,
                            .extra = .{ .expected_types = .{ .string_literal = true } },
                        });
                    }

                    const string_node = try self.state.arena.create(Node.StringTableString);
                    string_node.* = .{
                        .id = id_expression,
                        .maybe_comma = comma_token,
                        .string = self.state.token,
                    };
                    try strings.append(self.state.allocator, &string_node.base);
                }

                if (strings.items.len == 0) {
                    return self.addErrorDetailsAndFail(.{
                        .err = .expected_token, // TODO: probably a more specific error message
                        .token = self.state.token,
                        .extra = .{ .expected = .number },
                    });
                }

                const end_token = self.state.token;
                try self.check(.end);

                const node = try self.state.arena.create(Node.StringTable);
                node.* = .{
                    .type = first_token,
                    .common_resource_attributes = common_resource_attributes,
                    .optional_statements = optional_statements,
                    .begin_token = begin_token,
                    .strings = try self.state.arena.dupe(*Node, strings.items),
                    .end_token = end_token,
                };
                return &node.base;
            },
        };

        // The Win32 RC compiler allows for a 'dangling' literal at the end of a file
        // (as long as it's not a valid top-level keyword), and there is actually an
        // .rc file with a such a dangling literal in the Windows-classic-samples set
        // of projects. So, we have special compatibility for this particular case.
        const maybe_eof = try self.lookaheadToken(.whitespace_delimiter_only);
        if (maybe_eof.id == .eof) {
            try self.addErrorDetails(.{
                .err = .dangling_literal_at_eof,
                .type = .warning,
                .token = first_token,
            });

            var context = try self.state.arena.alloc(Token, 2);
            context[0] = first_token;
            context[1] = maybe_eof;
            const invalid_node = try self.state.arena.create(Node.Invalid);
            invalid_node.* = .{
                .context = context,
            };
            return &invalid_node.base;
        }

        const id_token = first_token;
        const id_code_page = self.lexer.current_code_page;
        try self.nextToken(.whitespace_delimiter_only);
        const resource = try self.checkResource();
        const type_token = self.state.token;

        if (resource == .string_num) {
            try self.addErrorDetails(.{
                .err = .string_resource_as_numeric_type,
                .token = type_token,
            });
            return self.addErrorDetailsAndFail(.{
                .err = .string_resource_as_numeric_type,
                .token = type_token,
                .type = .note,
                .print_source_line = false,
            });
        }

        if (resource == .font) {
            const id_bytes = SourceBytes{
                .slice = id_token.slice(self.lexer.buffer),
                .code_page = id_code_page,
            };
            const maybe_ordinal = res.NameOrOrdinal.maybeOrdinalFromString(id_bytes);
            if (maybe_ordinal == null) {
                const would_be_win32_rc_ordinal = res.NameOrOrdinal.maybeNonAsciiOrdinalFromString(id_bytes);
                if (would_be_win32_rc_ordinal) |win32_rc_ordinal| {
                    try self.addErrorDetails(.{
                        .err = .id_must_be_ordinal,
                        .token = id_token,
                        .extra = .{ .resource = resource },
                    });
                    return self.addErrorDetailsAndFail(.{
                        .err = .win32_non_ascii_ordinal,
                        .token = id_token,
                        .type = .note,
                        .print_source_line = false,
                        .extra = .{ .number = win32_rc_ordinal.ordinal },
                    });
                } else {
                    return self.addErrorDetailsAndFail(.{
                        .err = .id_must_be_ordinal,
                        .token = id_token,
                        .extra = .{ .resource = resource },
                    });
                }
            }
        }

        switch (resource) {
            .accelerators => {
                // common resource attributes must all be contiguous and come before optional-statements
                const common_resource_attributes = try self.parseCommonResourceAttributes();
                const optional_statements = try self.parseOptionalStatements(resource);

                try self.nextToken(.normal);
                const begin_token = self.state.token;
                try self.check(.begin);

                var accelerators: std.ArrayList(*Node) = .empty;

                while (true) {
                    const lookahead = try self.lookaheadToken(.normal);
                    switch (lookahead.id) {
                        .end, .eof => {
                            try self.nextToken(.normal);
                            break;
                        },
                        else => {},
                    }
                    const event = try self.parseExpression(.{ .allowed_types = .{ .number = true, .string = true } });

                    try self.nextToken(.normal);
                    try self.check(.comma);

                    const idvalue = try self.parseExpression(.{ .allowed_types = .{ .number = true } });

                    var type_and_options: std.ArrayList(Token) = .empty;
                    while (true) {
                        if (!(try self.parseOptionalToken(.comma))) break;

                        try self.nextToken(.normal);
                        if (!rc.AcceleratorTypeAndOptions.map.has(self.tokenSlice())) {
                            return self.addErrorDetailsAndFail(.{
                                .err = .expected_something_else,
                                .token = self.state.token,
                                .extra = .{ .expected_types = .{
                                    .accelerator_type_or_option = true,
                                } },
                            });
                        }
                        try type_and_options.append(self.state.arena, self.state.token);
                    }

                    const node = try self.state.arena.create(Node.Accelerator);
                    node.* = .{
                        .event = event,
                        .idvalue = idvalue,
                        .type_and_options = try type_and_options.toOwnedSlice(self.state.arena),
                    };
                    try accelerators.append(self.state.arena, &node.base);
                }

                const end_token = self.state.token;
                try self.check(.end);

                const node = try self.state.arena.create(Node.Accelerators);
                node.* = .{
                    .id = id_token,
                    .type = type_token,
                    .common_resource_attributes = common_resource_attributes,
                    .optional_statements = optional_statements,
                    .begin_token = begin_token,
                    .accelerators = try accelerators.toOwnedSlice(self.state.arena),
                    .end_token = end_token,
                };
                return &node.base;
            },
            .dialog, .dialogex => {
                // common resource attributes must all be contiguous and come before optional-statements
                const common_resource_attributes = try self.parseCommonResourceAttributes();

                const x = try self.parseExpression(.{ .allowed_types = .{ .number = true } });
                _ = try self.parseOptionalToken(.comma);

                const y = try self.parseExpression(.{ .allowed_types = .{ .number = true } });
                _ = try self.parseOptionalToken(.comma);

                const width = try self.parseExpression(.{ .allowed_types = .{ .number = true } });
                _ = try self.parseOptionalToken(.comma);

                const height = try self.parseExpression(.{ .allowed_types = .{ .number = true } });

                var optional_param_parser = OptionalParamParser{ .parser = self };
                const help_id: ?*Node = try optional_param_parser.parse(.{});

                const optional_statements = try self.parseOptionalStatements(resource);

                try self.nextToken(.normal);
                const begin_token = self.state.token;
                try self.check(.begin);

                var controls: std.ArrayList(*Node) = .empty;
                defer controls.deinit(self.state.allocator);
                while (try self.parseControlStatement(resource)) |control_node| {
                    // The number of controls must fit in a u16 in order for it to
                    // be able to be written into the relevant field in the .res data.
                    if (controls.items.len >= std.math.maxInt(u16)) {
                        try self.addErrorDetails(.{
                            .err = .too_many_dialog_controls_or_toolbar_buttons,
                            .token = id_token,
                            .extra = .{ .resource = resource },
                        });
                        return self.addErrorDetailsAndFail(.{
                            .err = .too_many_dialog_controls_or_toolbar_buttons,
                            .type = .note,
                            .token = control_node.getFirstToken(),
                            .token_span_end = control_node.getLastToken(),
                            .extra = .{ .resource = resource },
                        });
                    }

                    try controls.append(self.state.allocator, control_node);
                }

                try self.nextToken(.normal);
                const end_token = self.state.token;
                try self.check(.end);

                const node = try self.state.arena.create(Node.Dialog);
                node.* = .{
                    .id = id_token,
                    .type = type_token,
                    .common_resource_attributes = common_resource_attributes,
                    .x = x,
                    .y = y,
                    .width = width,
                    .height = height,
                    .help_id = help_id,
                    .optional_statements = optional_statements,
                    .begin_token = begin_token,
                    .controls = try self.state.arena.dupe(*Node, controls.items),
                    .end_token = end_token,
                };
                return &node.base;
            },
            .toolbar => {
                // common resource attributes must all be contiguous and come before optional-statements
                const common_resource_attributes = try self.parseCommonResourceAttributes();

                const button_width = try self.parseExpression(.{ .allowed_types = .{ .number = true } });

                try self.nextToken(.normal);
                try self.check(.comma);

                const button_height = try self.parseExpression(.{ .allowed_types = .{ .number = true } });

                try self.nextToken(.normal);
                const begin_token = self.state.token;
                try self.check(.begin);

                var buttons: std.ArrayList(*Node) = .empty;
                defer buttons.deinit(self.state.allocator);
                while (try self.parseToolbarButtonStatement()) |button_node| {
                    // The number of buttons must fit in a u16 in order for it to
                    // be able to be written into the relevant field in the .res data.
                    if (buttons.items.len >= std.math.maxInt(u16)) {
                        try self.addErrorDetails(.{
                            .err = .too_many_dialog_controls_or_toolbar_buttons,
                            .token = id_token,
                            .extra = .{ .resource = resource },
                        });
                        return self.addErrorDetailsAndFail(.{
                            .err = .too_many_dialog_controls_or_toolbar_buttons,
                            .type = .note,
                            .token = button_node.getFirstToken(),
                            .token_span_end = button_node.getLastToken(),
                            .extra = .{ .resource = resource },
                        });
                    }

                    try buttons.append(self.state.allocator, button_node);
                }

                try self.nextToken(.normal);
                const end_token = self.state.token;
                try self.check(.end);

                const node = try self.state.arena.create(Node.Toolbar);
                node.* = .{
                    .id = id_token,
                    .type = type_token,
                    .common_resource_attributes = common_resource_attributes,
                    .button_width = button_width,
                    .button_height = button_height,
                    .begin_token = begin_token,
                    .buttons = try self.state.arena.dupe(*Node, buttons.items),
                    .end_token = end_token,
                };
                return &node.base;
            },
            .menu, .menuex => {
                // common resource attributes must all be contiguous and come before optional-statements
                const common_resource_attributes = try self.parseCommonResourceAttributes();
                // help id is optional but must come between common resource attributes and optional-statements
                var help_id: ?*Node = null;
                // Note: No comma is allowed before or after help_id of MENUEX and help_id is not
                //       a possible field of MENU.
                if (resource == .menuex and try self.lookaheadCouldBeNumberExpression(.not_disallowed)) {
                    help_id = try self.parseExpression(.{
                        .is_known_to_be_number_expression = true,
                    });
                }
                const optional_statements = try self.parseOptionalStatements(.stringtable);

                try self.nextToken(.normal);
                const begin_token = self.state.token;
                try self.check(.begin);

                var items: std.ArrayList(*Node) = .empty;
                defer items.deinit(self.state.allocator);
                while (try self.parseMenuItemStatement(resource, id_token, 1)) |item_node| {
                    try items.append(self.state.allocator, item_node);
                }

                try self.nextToken(.normal);
                const end_token = self.state.token;
                try self.check(.end);

                if (items.items.len == 0) {
                    return self.addErrorDetailsAndFail(.{
                        .err = .empty_menu_not_allowed,
                        .token = type_token,
                    });
                }

                const node = try self.state.arena.create(Node.Menu);
                node.* = .{
                    .id = id_token,
                    .type = type_token,
                    .common_resource_attributes = common_resource_attributes,
                    .optional_statements = optional_statements,
                    .help_id = help_id,
                    .begin_token = begin_token,
                    .items = try self.state.arena.dupe(*Node, items.items),
                    .end_token = end_token,
                };
                return &node.base;
            },
            .versioninfo => {
                // common resource attributes must all be contiguous and come before optional-statements
                const common_resource_attributes = try self.parseCommonResourceAttributes();

                var fixed_info: std.ArrayList(*Node) = .empty;
                while (try self.parseVersionStatement()) |version_statement| {
                    try fixed_info.append(self.state.arena, version_statement);
                }

                try self.nextToken(.normal);
                const begin_token = self.state.token;
                try self.check(.begin);

                var block_statements: std.ArrayList(*Node) = .empty;
                while (try self.parseVersionBlockOrValue(id_token, 1)) |block_node| {
                    try block_statements.append(self.state.arena, block_node);
                }

                try self.nextToken(.normal);
                const end_token = self.state.token;
                try self.check(.end);

                const node = try self.state.arena.create(Node.VersionInfo);
                node.* = .{
                    .id = id_token,
                    .versioninfo = type_token,
                    .common_resource_attributes = common_resource_attributes,
                    .fixed_info = try fixed_info.toOwnedSlice(self.state.arena),
                    .begin_token = begin_token,
                    .block_statements = try block_statements.toOwnedSlice(self.state.arena),
                    .end_token = end_token,
                };
                return &node.base;
            },
            .dlginclude => {
                const common_resource_attributes = try self.parseCommonResourceAttributes();

                const filename_expression = try self.parseExpression(.{
                    .allowed_types = .{ .string = true },
                });

                const node = try self.state.arena.create(Node.ResourceExternal);
                node.* = .{
                    .id = id_token,
                    .type = type_token,
                    .common_resource_attributes = common_resource_attributes,
                    .filename = filename_expression,
                };
                return &node.base;
            },
            .stringtable => {
                return self.addErrorDetailsAndFail(.{
                    .err = .name_or_id_not_allowed,
                    .token = id_token,
                    .extra = .{ .resource = resource },
                });
            },
            // Just try everything as a 'generic' resource (raw data or external file)
            // TODO: More fine-grained switch cases as necessary
            else => {
                const common_resource_attributes = try self.parseCommonResourceAttributes();

                const maybe_begin = try self.lookaheadToken(.normal);
                if (maybe_begin.id == .begin) {
                    try self.nextToken(.normal);

                    if (!resource.canUseRawData()) {
                        try self.addErrorDetails(.{
                            .err = .resource_type_cant_use_raw_data,
                            .token = self.state.token,
                            .extra = .{ .resource = resource },
                        });
                        return self.addErrorDetailsAndFail(.{
                            .err = .resource_type_cant_use_raw_data,
                            .type = .note,
                            .print_source_line = false,
                            .token = self.state.token,
                        });
                    }

                    const raw_data = try self.parseRawDataBlock();
                    const end_token = self.state.token;

                    const node = try self.state.arena.create(Node.ResourceRawData);
                    node.* = .{
                        .id = id_token,
                        .type = type_token,
                        .common_resource_attributes = common_resource_attributes,
                        .begin_token = maybe_begin,
                        .raw_data = raw_data,
                        .end_token = end_token,
                    };
                    return &node.base;
                }

                const filename_expression = try self.parseExpression(.{
                    // Don't tell the user that numbers are accepted since we error on
                    // number expressions and regular number literals are treated as unquoted
                    // literals rather than numbers, so from the users perspective
                    // numbers aren't really allowed.
                    .expected_types_override = .{
                        .literal = true,
                        .string_literal = true,
                    },
                });

                const node = try self.state.arena.create(Node.ResourceExternal);
                node.* = .{
                    .id = id_token,
                    .type = type_token,
                    .common_resource_attributes = common_resource_attributes,
                    .filename = filename_expression,
                };
                return &node.base;
            },
        }
    }

    /// Expects the current token to be a begin token.
    /// After return, the current token will be the end token.
    fn parseRawDataBlock(self: *Self) Error![]*Node {
        var raw_data: std.ArrayList(*Node) = .empty;
        defer raw_data.deinit(self.state.allocator);
        while (true) {
            const maybe_end_token = try self.lookaheadToken(.normal);
            switch (maybe_end_token.id) {
                .comma => {
                    try self.nextToken(.normal);
                    // comma as the first token in a raw data block is an error
                    if (raw_data.items.len == 0) {
                        return self.addErrorDetailsAndFail(.{
                            .err = .expected_something_else,
                            .token = self.state.token,
                            .extra = .{ .expected_types = .{
                                .number = true,
                                .number_expression = true,
                                .string_literal = true,
                            } },
                        });
                    }
                    // otherwise just skip over commas
                    continue;
                },
                .end => {
                    try self.nextToken(.normal);
                    break;
                },
                .eof => {
                    return self.addErrorDetailsWithCodePageAndFail(.{
                        .err = .unfinished_raw_data_block,
                        .code_page = self.lexer.current_code_page,
                        .token = maybe_end_token,
                    });
                },
                else => {},
            }
            const expression = try self.parseExpression(.{ .allowed_types = .{ .number = true, .string = true } });
            try raw_data.append(self.state.allocator, expression);

            if (expression.isNumberExpression()) {
                const maybe_close_paren = try self.lookaheadToken(.normal);
                if (maybe_close_paren.id == .close_paren) {
                    // advance to ensure that the code page lookup is populated for this token
                    try self.nextToken(.normal);
                    // <number expression>) is an error
                    return self.addErrorDetailsAndFail(.{
                        .err = .expected_token,
                        .token = self.state.token,
                        .extra = .{ .expected = .operator },
                    });
                }
            }
        }
        return try self.state.arena.dupe(*Node, raw_data.items);
    }

    /// Expects the current token to be handled, and that the control statement will
    /// begin on the next token.
    /// After return, the current token will be the token immediately before the end of the
    /// control statement (or unchanged if the function returns null).
    fn parseControlStatement(self: *Self, resource: ResourceType) Error!?*Node {
        const control_token = try self.lookaheadToken(.normal);
        const control = rc.Control.map.get(control_token.slice(self.lexer.buffer)) orelse return null;
        try self.nextToken(.normal);

        try self.skipAnyCommas();

        var text: ?Token = null;
        if (control.hasTextParam()) {
            try self.nextToken(.normal);
            switch (self.state.token.id) {
                .quoted_ascii_string, .quoted_wide_string, .number => {
                    text = self.state.token;
                },
                else => {
                    return self.addErrorDetailsAndFail(.{
                        .err = .expected_something_else,
                        .token = self.state.token,
                        .extra = .{ .expected_types = .{
                            .number = true,
                            .string_literal = true,
                        } },
                    });
                },
            }
            try self.skipAnyCommas();
        }

        const id = try self.parseExpression(.{ .allowed_types = .{ .number = true } });

        try self.skipAnyCommas();

        var class: ?*Node = null;
        var style: ?*Node = null;
        if (control == .control) {
            class = try self.parseExpression(.{});
            if (class.?.id == .literal) {
                const class_literal: *Node.Literal = @alignCast(@fieldParentPtr("base", class.?));
                const is_invalid_control_class = class_literal.token.id == .literal and !rc.ControlClass.map.has(class_literal.token.slice(self.lexer.buffer));
                if (is_invalid_control_class) {
                    return self.addErrorDetailsAndFail(.{
                        .err = .expected_something_else,
                        .token = self.state.token,
                        .extra = .{ .expected_types = .{
                            .control_class = true,
                        } },
                    });
                }
            }
            try self.skipAnyCommas();
            style = try self.parseExpression(.{
                .can_contain_not_expressions = true,
                .allowed_types = .{ .number = true },
            });
            // If there is no comma after the style paramter, the Win32 RC compiler
            // could misinterpret the statement and end up skipping over at least one token
            // that should have been interepeted as the next parameter (x). For example:
            //   CONTROL "text", 1, BUTTON, 15 30, 1, 2, 3, 4
            // the `15` is the style parameter, but in the Win32 implementation the `30`
            // is completely ignored (i.e. the `1, 2, 3, 4` are `x`, `y`, `w`, `h`).
            // If a comma is added after the `15`, then `30` gets interpreted (correctly)
            // as the `x` value.
            //
            // Instead of emulating this behavior, we just warn about the potential for
            // weird behavior in the Win32 implementation whenever there isn't a comma after
            // the style parameter.
            const lookahead_token = try self.lookaheadToken(.normal);
            if (lookahead_token.id != .comma and lookahead_token.id != .eof) {
                try self.addErrorDetailsWithCodePage(.{
                    .err = .rc_could_miscompile_control_params,
                    .type = .warning,
                    .code_page = self.lexer.current_code_page,
                    .token = lookahead_token,
                });
                try self.addErrorDetailsWithCodePage(.{
                    .err = .rc_could_miscompile_control_params,
                    .type = .note,
                    .code_page = self.lexer.current_code_page,
                    .token = style.?.getFirstToken(),
                    .token_span_end = style.?.getLastToken(),
                });
            }
            try self.skipAnyCommas();
        }

        const x = try self.parseExpression(.{ .allowed_types = .{ .number = true } });
        _ = try self.parseOptionalToken(.comma);
        const y = try self.parseExpression(.{ .allowed_types = .{ .number = true } });
        _ = try self.parseOptionalToken(.comma);
        const width = try self.parseExpression(.{ .allowed_types = .{ .number = true } });
        _ = try self.parseOptionalToken(.comma);
        const height = try self.parseExpression(.{ .allowed_types = .{ .number = true } });

        var optional_param_parser = OptionalParamParser{ .parser = self };
        if (control != .control) {
            style = try optional_param_parser.parse(.{ .not_expression_allowed = true });
        }

        const exstyle: ?*Node = try optional_param_parser.parse(.{ .not_expression_allowed = true });
        const help_id: ?*Node = switch (resource) {
            .dialogex => try optional_param_parser.parse(.{}),
            else => null,
        };

        var extra_data: []*Node = &[_]*Node{};
        var extra_data_begin: ?Token = null;
        var extra_data_end: ?Token = null;
        // extra data is DIALOGEX-only
        if (resource == .dialogex and try self.parseOptionalToken(.begin)) {
            extra_data_begin = self.state.token;
            extra_data = try self.parseRawDataBlock();
            extra_data_end = self.state.token;
        }

        const node = try self.state.arena.create(Node.ControlStatement);
        node.* = .{
            .type = control_token,
            .text = text,
            .class = class,
            .id = id,
            .x = x,
            .y = y,
            .width = width,
            .height = height,
            .style = style,
            .exstyle = exstyle,
            .help_id = help_id,
            .extra_data_begin = extra_data_begin,
            .extra_data = extra_data,
            .extra_data_end = extra_data_end,
        };
        return &node.base;
    }

    fn parseToolbarButtonStatement(self: *Self) Error!?*Node {
        const keyword_token = try self.lookaheadToken(.normal);
        const button_type = rc.ToolbarButton.map.get(keyword_token.slice(self.lexer.buffer)) orelse return null;
        try self.nextToken(.normal);

        switch (button_type) {
            .separator => {
                const node = try self.state.arena.create(Node.Literal);
                node.* = .{
                    .token = keyword_token,
                };
                return &node.base;
            },
            .button => {
                const button_id = try self.parseExpression(.{ .allowed_types = .{ .number = true } });

                const node = try self.state.arena.create(Node.SimpleStatement);
                node.* = .{
                    .identifier = keyword_token,
                    .value = button_id,
                };
                return &node.base;
            },
        }
    }

    /// Expects the current token to be handled, and that the menuitem/popup statement will
    /// begin on the next token.
    /// After return, the current token will be the token immediately before the end of the
    /// menuitem statement (or unchanged if the function returns null).
    fn parseMenuItemStatement(self: *Self, resource: ResourceType, top_level_menu_id_token: Token, nesting_level: u32) Error!?*Node {
        const menuitem_token = try self.lookaheadToken(.normal);
        const menuitem = rc.MenuItem.map.get(menuitem_token.slice(self.lexer.buffer)) orelse return null;
        try self.nextToken(.normal);

        if (nesting_level > max_nested_menu_level) {
            try self.addErrorDetails(.{
                .err = .nested_resource_level_exceeds_max,
                .token = top_level_menu_id_token,
                .extra = .{ .resource = resource },
            });
            return self.addErrorDetailsAndFail(.{
                .err = .nested_resource_level_exceeds_max,
                .type = .note,
                .token = menuitem_token,
                .extra = .{ .resource = resource },
            });
        }

        switch (resource) {
            .menu => switch (menuitem) {
                .menuitem => {
                    try self.nextToken(.normal);
                    if (rc.MenuItem.isSeparator(self.state.token.slice(self.lexer.buffer))) {
                        const separator_token = self.state.token;
                        // There can be any number of trailing commas after SEPARATOR
                        try self.skipAnyCommas();
                        const node = try self.state.arena.create(Node.MenuItemSeparator);
                        node.* = .{
                            .menuitem = menuitem_token,
                            .separator = separator_token,
                        };
                        return &node.base;
                    } else {
                        const text = self.state.token;
                        if (!text.isStringLiteral()) {
                            return self.addErrorDetailsAndFail(.{
                                .err = .expected_something_else,
                                .token = text,
                                .extra = .{ .expected_types = .{
                                    .string_literal = true,
                                } },
                            });
                        }
                        try self.skipAnyCommas();

                        const result = try self.parseExpression(.{ .allowed_types = .{ .number = true } });

                        _ = try self.parseOptionalToken(.comma);

                        var options: std.ArrayList(Token) = .empty;
                        while (true) {
                            const option_token = try self.lookaheadToken(.normal);
                            if (!rc.MenuItem.Option.map.has(option_token.slice(self.lexer.buffer))) {
                                break;
                            }
                            try self.nextToken(.normal);
                            try options.append(self.state.arena, option_token);
                            try self.skipAnyCommas();
                        }

                        const node = try self.state.arena.create(Node.MenuItem);
                        node.* = .{
                            .menuitem = menuitem_token,
                            .text = text,
                            .result = result,
                            .option_list = try options.toOwnedSlice(self.state.arena),
                        };
                        return &node.base;
                    }
                },
                .popup => {
                    try self.nextToken(.normal);
                    const text = self.state.token;
                    if (!text.isStringLiteral()) {
                        return self.addErrorDetailsAndFail(.{
                            .err = .expected_something_else,
                            .token = text,
                            .extra = .{ .expected_types = .{
                                .string_literal = true,
                            } },
                        });
                    }
                    try self.skipAnyCommas();

                    var options: std.ArrayList(Token) = .empty;
                    while (true) {
                        const option_token = try self.lookaheadToken(.normal);
                        if (!rc.MenuItem.Option.map.has(option_token.slice(self.lexer.buffer))) {
                            break;
                        }
                        try self.nextToken(.normal);
                        try options.append(self.state.arena, option_token);
                        try self.skipAnyCommas();
                    }

                    try self.nextToken(.normal);
                    const begin_token = self.state.token;
                    try self.check(.begin);

                    var items: std.ArrayList(*Node) = .empty;
                    while (try self.parseMenuItemStatement(resource, top_level_menu_id_token, nesting_level + 1)) |item_node| {
                        try items.append(self.state.arena, item_node);
                    }

                    try self.nextToken(.normal);
                    const end_token = self.state.token;
                    try self.check(.end);

                    if (items.items.len == 0) {
                        return self.addErrorDetailsAndFail(.{
                            .err = .empty_menu_not_allowed,
                            .token = menuitem_token,
                        });
                    }

                    const node = try self.state.arena.create(Node.Popup);
                    node.* = .{
                        .popup = menuitem_token,
                        .text = text,
                        .option_list = try options.toOwnedSlice(self.state.arena),
                        .begin_token = begin_token,
                        .items = try items.toOwnedSlice(self.state.arena),
                        .end_token = end_token,
                    };
                    return &node.base;
                },
            },
            .menuex => {
                try self.nextToken(.normal);
                const text = self.state.token;
                if (!text.isStringLiteral()) {
                    return self.addErrorDetailsAndFail(.{
                        .err = .expected_something_else,
                        .token = text,
                        .extra = .{ .expected_types = .{
                            .string_literal = true,
                        } },
                    });
                }

                var param_parser = OptionalParamParser{ .parser = self };
                const id = try param_parser.parse(.{});
                const item_type = try param_parser.parse(.{});
                const state = try param_parser.parse(.{});

                if (menuitem == .menuitem) {
                    // trailing comma is allowed, skip it
                    _ = try self.parseOptionalToken(.comma);

                    const node = try self.state.arena.create(Node.MenuItemEx);
                    node.* = .{
                        .menuitem = menuitem_token,
                        .text = text,
                        .id = id,
                        .type = item_type,
                        .state = state,
                    };
                    return &node.base;
                }

                const help_id = try param_parser.parse(.{});

                // trailing comma is allowed, skip it
                _ = try self.parseOptionalToken(.comma);

                try self.nextToken(.normal);
                const begin_token = self.state.token;
                try self.check(.begin);

                var items: std.ArrayList(*Node) = .empty;
                while (try self.parseMenuItemStatement(resource, top_level_menu_id_token, nesting_level + 1)) |item_node| {
                    try items.append(self.state.arena, item_node);
                }

                try self.nextToken(.normal);
                const end_token = self.state.token;
                try self.check(.end);

                if (items.items.len == 0) {
                    return self.addErrorDetailsAndFail(.{
                        .err = .empty_menu_not_allowed,
                        .token = menuitem_token,
                    });
                }

                const node = try self.state.arena.create(Node.PopupEx);
                node.* = .{
                    .popup = menuitem_token,
                    .text = text,
                    .id = id,
                    .type = item_type,
                    .state = state,
                    .help_id = help_id,
                    .begin_token = begin_token,
                    .items = try items.toOwnedSlice(self.state.arena),
                    .end_token = end_token,
                };
                return &node.base;
            },
            else => unreachable,
        }
        @compileError("unreachable");
    }

    pub const OptionalParamParser = struct {
        finished: bool = false,
        parser: *Self,

        pub const Options = struct {
            not_expression_allowed: bool = false,
        };

        pub fn parse(self: *OptionalParamParser, options: OptionalParamParser.Options) Error!?*Node {
            if (self.finished) return null;
            if (!(try self.parser.parseOptionalToken(.comma))) {
                self.finished = true;
                return null;
            }
            // If the next lookahead token could be part of a number expression,
            // then parse it. Otherwise, treat it as an 'empty' expression and
            // continue parsing, since 'empty' values are allowed.
            if (try self.parser.lookaheadCouldBeNumberExpression(switch (options.not_expression_allowed) {
                true => .not_allowed,
                false => .not_disallowed,
            })) {
                const node = try self.parser.parseExpression(.{
                    .allowed_types = .{ .number = true },
                    .can_contain_not_expressions = options.not_expression_allowed,
                });
                return node;
            }
            return null;
        }
    };

    /// Expects the current token to be handled, and that the version statement will
    /// begin on the next token.
    /// After return, the current token will be the token immediately before the end of the
    /// version statement (or unchanged if the function returns null).
    fn parseVersionStatement(self: *Self) Error!?*Node {
        const type_token = try self.lookaheadToken(.normal);
        const statement_type = rc.VersionInfo.map.get(type_token.slice(self.lexer.buffer)) orelse return null;
        try self.nextToken(.normal);
        switch (statement_type) {
            .file_version, .product_version => {
                var parts_buffer: [4]*Node = undefined;
                var parts = std.ArrayList(*Node).initBuffer(&parts_buffer);

                while (true) {
                    const value = try self.parseExpression(.{ .allowed_types = .{ .number = true } });
                    parts.addOneAssumeCapacity().* = value;

                    if (parts.unusedCapacitySlice().len == 0 or
                        !(try self.parseOptionalToken(.comma)))
                    {
                        break;
                    }
                }

                const node = try self.state.arena.create(Node.VersionStatement);
                node.* = .{
                    .type = type_token,
                    .parts = try self.state.arena.dupe(*Node, parts.items),
                };
                return &node.base;
            },
            else => {
                const value = try self.parseExpression(.{ .allowed_types = .{ .number = true } });

                const node = try self.state.arena.create(Node.SimpleStatement);
                node.* = .{
                    .identifier = type_token,
                    .value = value,
                };
                return &node.base;
            },
        }
    }

    /// Expects the current token to be handled, and that the version BLOCK/VALUE will
    /// begin on the next token.
    /// After return, the current token will be the token immediately before the end of the
    /// version BLOCK/VALUE (or unchanged if the function returns null).
    fn parseVersionBlockOrValue(self: *Self, top_level_version_id_token: Token, nesting_level: u32) Error!?*Node {
        const keyword_token = try self.lookaheadToken(.normal);
        const keyword = rc.VersionBlock.map.get(keyword_token.slice(self.lexer.buffer)) orelse return null;
        try self.nextToken(.normal);

        if (nesting_level > max_nested_version_level) {
            try self.addErrorDetails(.{
                .err = .nested_resource_level_exceeds_max,
                .token = top_level_version_id_token,
                .extra = .{ .resource = .versioninfo },
            });
            return self.addErrorDetailsAndFail(.{
                .err = .nested_resource_level_exceeds_max,
                .type = .note,
                .token = keyword_token,
                .extra = .{ .resource = .versioninfo },
            });
        }

        try self.nextToken(.normal);
        const key = self.state.token;
        if (!key.isStringLiteral()) {
            return self.addErrorDetailsAndFail(.{
                .err = .expected_something_else,
                .token = key,
                .extra = .{ .expected_types = .{
                    .string_literal = true,
                } },
            });
        }
        // Need to keep track of this to detect a potential miscompilation when
        // the comma is omitted and the first value is a quoted string.
        const had_comma_before_first_value = try self.parseOptionalToken(.comma);
        try self.skipAnyCommas();

        const values = try self.parseBlockValuesList(had_comma_before_first_value);

        switch (keyword) {
            .block => {
                try self.nextToken(.normal);
                const begin_token = self.state.token;
                try self.check(.begin);

                var children: std.ArrayList(*Node) = .empty;
                while (try self.parseVersionBlockOrValue(top_level_version_id_token, nesting_level + 1)) |value_node| {
                    try children.append(self.state.arena, value_node);
                }

                try self.nextToken(.normal);
                const end_token = self.state.token;
                try self.check(.end);

                const node = try self.state.arena.create(Node.Block);
                node.* = .{
                    .identifier = keyword_token,
                    .key = key,
                    .values = values,
                    .begin_token = begin_token,
                    .children = try children.toOwnedSlice(self.state.arena),
                    .end_token = end_token,
                };
                return &node.base;
            },
            .value => {
                const node = try self.state.arena.create(Node.BlockValue);
                node.* = .{
                    .identifier = keyword_token,
                    .key = key,
                    .values = values,
                };
                return &node.base;
            },
        }
    }

    fn parseBlockValuesList(self: *Self, had_comma_before_first_value: bool) Error![]*Node {
        var values: std.ArrayList(*Node) = .empty;
        var seen_number: bool = false;
        var first_string_value: ?*Node = null;
        while (true) {
            const lookahead_token = try self.lookaheadToken(.normal);
            switch (lookahead_token.id) {
                .operator,
                .number,
                .open_paren,
                .quoted_ascii_string,
                .quoted_wide_string,
                => {},
                else => break,
            }
            const value = try self.parseExpression(.{});

            if (value.isNumberExpression()) {
                seen_number = true;
            } else if (first_string_value == null) {
                std.debug.assert(value.isStringLiteral());
                first_string_value = value;
            }

            const has_trailing_comma = try self.parseOptionalToken(.comma);
            try self.skipAnyCommas();

            const value_value = try self.state.arena.create(Node.BlockValueValue);
            value_value.* = .{
                .expression = value,
                .trailing_comma = has_trailing_comma,
            };
            try values.append(self.state.arena, &value_value.base);
        }
        if (seen_number and first_string_value != null) {
            // The Win32 RC compiler does some strange stuff with the data size:
            // Strings are counted as UTF-16 code units including the null-terminator
            // Numbers are counted as their byte lengths
            // So, when both strings and numbers are within a single value,
            // it incorrectly sets the value's type as binary, but then gives the
            // data length as a mixture of bytes and UTF-16 code units. This means that
            // when the length is read, it will be treated as byte length and will
            // not read the full value. We don't reproduce this behavior, so we warn
            // of the miscompilation here.
            try self.addErrorDetails(.{
                .err = .rc_would_miscompile_version_value_byte_count,
                .type = .warning,
                .token = first_string_value.?.getFirstToken(),
                .token_span_start = values.items[0].getFirstToken(),
                .token_span_end = values.items[values.items.len - 1].getLastToken(),
            });
            try self.addErrorDetails(.{
                .err = .rc_would_miscompile_version_value_byte_count,
                .type = .note,
                .token = first_string_value.?.getFirstToken(),
                .token_span_start = values.items[0].getFirstToken(),
                .token_span_end = values.items[values.items.len - 1].getLastToken(),
                .print_source_line = false,
            });
        }
        if (!had_comma_before_first_value and values.items.len > 0 and values.items[0].cast(.block_value_value).?.expression.isStringLiteral()) {
            const token = values.items[0].cast(.block_value_value).?.expression.cast(.literal).?.token;
            try self.addErrorDetails(.{
                .err = .rc_would_miscompile_version_value_padding,
                .type = .warning,
                .token = token,
            });
            try self.addErrorDetails(.{
                .err = .rc_would_miscompile_version_value_padding,
                .type = .note,
                .token = token,
                .print_source_line = false,
            });
        }
        return values.toOwnedSlice(self.state.arena);
    }

    fn numberExpressionContainsAnyLSuffixes(expression_node: *Node, source: []const u8, code_page_lookup: *const CodePageLookup) bool {
        // TODO: This could probably be done without evaluating the whole expression
        return Compiler.evaluateNumberExpression(expression_node, source, code_page_lookup).is_long;
    }

    /// Expects the current token to be a literal token that contains the string LANGUAGE
    fn parseLanguageStatement(self: *Self) Error!*Node {
        const language_token = self.state.token;

        const primary_language = try self.parseExpression(.{ .allowed_types = .{ .number = true } });

        try self.nextToken(.normal);
        try self.check(.comma);

        const sublanguage = try self.parseExpression(.{ .allowed_types = .{ .number = true } });

        // The Win32 RC compiler errors if either parameter contains any number with an L
        // suffix. Instead of that, we want to warn and then let the values get truncated.
        // The warning is done here to allow the compiler logic to not have to deal with this.
        if (numberExpressionContainsAnyLSuffixes(primary_language, self.lexer.buffer, &self.state.input_code_page_lookup)) {
            try self.addErrorDetails(.{
                .err = .rc_would_error_u16_with_l_suffix,
                .type = .warning,
                .token = primary_language.getFirstToken(),
                .token_span_end = primary_language.getLastToken(),
                .extra = .{ .statement_with_u16_param = .language },
            });
            try self.addErrorDetails(.{
                .err = .rc_would_error_u16_with_l_suffix,
                .print_source_line = false,
                .type = .note,
                .token = primary_language.getFirstToken(),
                .token_span_end = primary_language.getLastToken(),
                .extra = .{ .statement_with_u16_param = .language },
            });
        }
        if (numberExpressionContainsAnyLSuffixes(sublanguage, self.lexer.buffer, &self.state.input_code_page_lookup)) {
            try self.addErrorDetails(.{
                .err = .rc_would_error_u16_with_l_suffix,
                .type = .warning,
                .token = sublanguage.getFirstToken(),
                .token_span_end = sublanguage.getLastToken(),
                .extra = .{ .statement_with_u16_param = .language },
            });
            try self.addErrorDetails(.{
                .err = .rc_would_error_u16_with_l_suffix,
                .print_source_line = false,
                .type = .note,
                .token = sublanguage.getFirstToken(),
                .token_span_end = sublanguage.getLastToken(),
                .extra = .{ .statement_with_u16_param = .language },
            });
        }

        const node = try self.state.arena.create(Node.LanguageStatement);
        node.* = .{
            .language_token = language_token,
            .primary_language_id = primary_language,
            .sublanguage_id = sublanguage,
        };
        return &node.base;
    }

    pub const ParseExpressionOptions = struct {
        is_known_to_be_number_expression: bool = false,
        can_contain_not_expressions: bool = false,
        nesting_context: NestingContext = .{},
        allowed_types: AllowedTypes = .{ .literal = true, .number = true, .string = true },
        expected_types_override: ?ErrorDetails.ExpectedTypes = null,

        pub const AllowedTypes = struct {
            literal: bool = false,
            number: bool = false,
            string: bool = false,
        };

        pub const NestingContext = struct {
            first_token: ?Token = null,
            last_token: ?Token = null,
            level: u32 = 0,

            /// Returns a new NestingContext with values modified appropriately for an increased nesting level
            fn incremented(ctx: NestingContext, first_token: Token, most_recent_token: Token) NestingContext {
                return .{
                    .first_token = ctx.first_token orelse first_token,
                    .last_token = most_recent_token,
                    .level = ctx.level + 1,
                };
            }
        };

        pub fn toErrorDetails(options: ParseExpressionOptions, token: Token) ErrorDetailsWithoutCodePage {
            // TODO: expected_types_override interaction with is_known_to_be_number_expression?
            const expected_types = options.expected_types_override orelse ErrorDetails.ExpectedTypes{
                .number = options.allowed_types.number,
                .number_expression = options.allowed_types.number,
                .string_literal = options.allowed_types.string and !options.is_known_to_be_number_expression,
                .literal = options.allowed_types.literal and !options.is_known_to_be_number_expression,
            };
            return .{
                .err = .expected_something_else,
                .token = token,
                .extra = .{ .expected_types = expected_types },
            };
        }
    };

    /// Returns true if the next lookahead token is a number or could be the start of a number expression.
    /// Only useful when looking for empty expressions in optional fields.
    fn lookaheadCouldBeNumberExpression(self: *Self, not_allowed: enum { not_allowed, not_disallowed }) Error!bool {
        var lookahead_token = try self.lookaheadToken(.normal);
        switch (lookahead_token.id) {
            .literal => if (not_allowed == .not_allowed) {
                return std.ascii.eqlIgnoreCase("NOT", lookahead_token.slice(self.lexer.buffer));
            } else return false,
            .number => return true,
            .open_paren => return true,
            .operator => {
                // + can be a unary operator, see parseExpression's handling of unary +
                const operator_char = lookahead_token.slice(self.lexer.buffer)[0];
                return operator_char == '+';
            },
            else => return false,
        }
    }

    fn parsePrimary(self: *Self, options: ParseExpressionOptions) Error!*Node {
        try self.nextToken(.normal);
        const first_token = self.state.token;
        var is_close_paren_expression = false;
        var is_unary_plus_expression = false;
        switch (self.state.token.id) {
            .quoted_ascii_string, .quoted_wide_string => {
                if (!options.allowed_types.string) return self.addErrorDetailsAndFail(options.toErrorDetails(self.state.token));
                const node = try self.state.arena.create(Node.Literal);
                node.* = .{ .token = self.state.token };
                return &node.base;
            },
            .literal => {
                if (options.can_contain_not_expressions and std.ascii.eqlIgnoreCase("NOT", self.state.token.slice(self.lexer.buffer))) {
                    const not_token = self.state.token;
                    try self.nextToken(.normal);
                    try self.check(.number);
                    if (!options.allowed_types.number) return self.addErrorDetailsAndFail(options.toErrorDetails(self.state.token));
                    const node = try self.state.arena.create(Node.NotExpression);
                    node.* = .{
                        .not_token = not_token,
                        .number_token = self.state.token,
                    };
                    return &node.base;
                }
                if (!options.allowed_types.literal) return self.addErrorDetailsAndFail(options.toErrorDetails(self.state.token));
                const node = try self.state.arena.create(Node.Literal);
                node.* = .{ .token = self.state.token };
                return &node.base;
            },
            .number => {
                if (!options.allowed_types.number) return self.addErrorDetailsAndFail(options.toErrorDetails(self.state.token));
                const node = try self.state.arena.create(Node.Literal);
                node.* = .{ .token = self.state.token };
                return &node.base;
            },
            .open_paren => {
                const open_paren_token = self.state.token;

                const expression = try self.parseExpression(.{
                    .is_known_to_be_number_expression = true,
                    .can_contain_not_expressions = options.can_contain_not_expressions,
                    .nesting_context = options.nesting_context.incremented(first_token, open_paren_token),
                    .allowed_types = .{ .number = true },
                });

                try self.nextToken(.normal);
                // TODO: Add context to error about where the open paren is
                try self.check(.close_paren);

                if (!options.allowed_types.number) return self.addErrorDetailsAndFail(options.toErrorDetails(open_paren_token));
                const node = try self.state.arena.create(Node.GroupedExpression);
                node.* = .{
                    .open_token = open_paren_token,
                    .expression = expression,
                    .close_token = self.state.token,
                };
                return &node.base;
            },
            .close_paren => {
                // Note: In the Win32 implementation, a single close paren
                // counts as a valid "expression", but only when its the first and
                // only token in the expression. Such an expression is then treated
                // as a 'skip this expression' instruction. For example:
                //   1 RCDATA { 1, ), ), ), 2 }
                // will be evaluated as if it were `1 RCDATA { 1, 2 }` and only
                // 0x0001 and 0x0002 will be written to the .res data.
                //
                // This behavior is not emulated because it almost certainly has
                // no valid use cases and only introduces edge cases that are
                // not worth the effort to track down and deal with. Instead,
                // we error but also add a note about the Win32 RC behavior if
                // this edge case is detected.
                if (!options.is_known_to_be_number_expression) {
                    is_close_paren_expression = true;
                }
            },
            .operator => {
                // In the Win32 implementation, something akin to a unary +
                // is allowed but it doesn't behave exactly like a unary +.
                // Instead of emulating the Win32 behavior, we instead error
                // and add a note about unary plus not being allowed.
                //
                // This is done because unary + only works in some places,
                // and there's no real use-case for it since it's so limited
                // in how it can be used (e.g. +1 is accepted but (+1) will error)
                //
                // Even understanding when unary plus is allowed is difficult, so
                // we don't do any fancy detection of when the Win32 RC compiler would
                // allow a unary + and instead just output the note in all cases.
                //
                // Some examples of allowed expressions by the Win32 compiler:
                //  +1
                //  0|+5
                //  +1+2
                //  +~-5
                //  +(1)
                //
                // Some examples of disallowed expressions by the Win32 compiler:
                //  (+1)
                //  ++5
                //
                // TODO: Potentially re-evaluate and support the unary plus in a bug-for-bug
                //       compatible way.
                const operator_char = self.state.token.slice(self.lexer.buffer)[0];
                if (operator_char == '+') {
                    is_unary_plus_expression = true;
                }
            },
            else => {},
        }

        try self.addErrorDetails(options.toErrorDetails(self.state.token));
        if (is_close_paren_expression) {
            try self.addErrorDetails(.{
                .err = .close_paren_expression,
                .type = .note,
                .token = self.state.token,
                .print_source_line = false,
            });
        }
        if (is_unary_plus_expression) {
            try self.addErrorDetails(.{
                .err = .unary_plus_expression,
                .type = .note,
                .token = self.state.token,
                .print_source_line = false,
            });
        }
        return error.ParseError;
    }

    /// Expects the current token to have already been dealt with, and that the
    /// expression will start on the next token.
    /// After return, the current token will have been dealt with.
    fn parseExpression(self: *Self, options: ParseExpressionOptions) Error!*Node {
        if (options.nesting_context.level > max_nested_expression_level) {
            try self.addErrorDetails(.{
                .err = .nested_expression_level_exceeds_max,
                .token = options.nesting_context.first_token.?,
            });
            return self.addErrorDetailsAndFail(.{
                .err = .nested_expression_level_exceeds_max,
                .type = .note,
                .token = options.nesting_context.last_token.?,
            });
        }
        var expr: *Node = try self.parsePrimary(options);
        const first_token = expr.getFirstToken();

        // Non-number expressions can't have operators, so we can just return
        if (!expr.isNumberExpression()) return expr;

        while (try self.parseOptionalTokenAdvanced(.operator, .normal_expect_operator)) {
            const operator = self.state.token;
            const rhs_node = try self.parsePrimary(.{
                .is_known_to_be_number_expression = true,
                .can_contain_not_expressions = options.can_contain_not_expressions,
                .nesting_context = options.nesting_context.incremented(first_token, operator),
                .allowed_types = options.allowed_types,
            });

            if (!rhs_node.isNumberExpression()) {
                return self.addErrorDetailsAndFail(.{
                    .err = .expected_something_else,
                    .token = rhs_node.getFirstToken(),
                    .token_span_end = rhs_node.getLastToken(),
                    .extra = .{ .expected_types = .{
                        .number = true,
                        .number_expression = true,
                    } },
                });
            }

            const node = try self.state.arena.create(Node.BinaryExpression);
            node.* = .{
                .left = expr,
                .operator = operator,
                .right = rhs_node,
            };
            expr = &node.base;
        }

        return expr;
    }

    /// Skips any amount of commas (including zero)
    /// In other words, it will skip the regex `,*`
    /// Assumes the token(s) should be parsed with `.normal` as the method.
    fn skipAnyCommas(self: *Self) !void {
        while (try self.parseOptionalToken(.comma)) {}
    }

    /// Advances the current token only if the token's id matches the specified `id`.
    /// Assumes the token should be parsed with `.normal` as the method.
    /// Returns true if the token matched, false otherwise.
    fn parseOptionalToken(self: *Self, id: Token.Id) Error!bool {
        return self.parseOptionalTokenAdvanced(id, .normal);
    }

    /// Advances the current token only if the token's id matches the specified `id`.
    /// Returns true if the token matched, false otherwise.
    fn parseOptionalTokenAdvanced(self: *Self, id: Token.Id, comptime method: Lexer.LexMethod) Error!bool {
        const maybe_token = try self.lookaheadToken(method);
        if (maybe_token.id != id) return false;
        try self.nextToken(method);
        return true;
    }

    fn addErrorDetailsWithCodePage(self: *Self, details: ErrorDetails) Allocator.Error!void {
        try self.state.diagnostics.append(details);
    }

    fn addErrorDetailsWithCodePageAndFail(self: *Self, details: ErrorDetails) Error {
        try self.addErrorDetailsWithCodePage(details);
        return error.ParseError;
    }

    /// Code page is looked up in input_code_page_lookup using the token, meaning the token
    /// must come from nextToken (i.e. it can't be a lookahead token).
    fn addErrorDetails(self: *Self, details_without_code_page: ErrorDetailsWithoutCodePage) Allocator.Error!void {
        const details = ErrorDetails{
            .err = details_without_code_page.err,
            .code_page = self.state.input_code_page_lookup.getForToken(details_without_code_page.token),
            .token = details_without_code_page.token,
            .token_span_start = details_without_code_page.token_span_start,
            .token_span_end = details_without_code_page.token_span_end,
            .type = details_without_code_page.type,
            .print_source_line = details_without_code_page.print_source_line,
            .extra = details_without_code_page.extra,
        };
        try self.addErrorDetailsWithCodePage(details);
    }

    /// Code page is looked up in input_code_page_lookup using the token, meaning the token
    /// must come from nextToken (i.e. it can't be a lookahead token).
    fn addErrorDetailsAndFail(self: *Self, details_without_code_page: ErrorDetailsWithoutCodePage) Error {
        try self.addErrorDetails(details_without_code_page);
        return error.ParseError;
    }

    fn nextToken(self: *Self, comptime method: Lexer.LexMethod) Error!void {
        self.state.token = token: while (true) {
            const token = self.lexer.next(method) catch |err| switch (err) {
                error.CodePagePragmaInIncludedFile => {
                    // The Win32 RC compiler silently ignores such `#pragma code_page` directives,
                    // but we want to both ignore them *and* emit a warning
                    var details = self.lexer.getErrorDetails(err);
                    details.type = .warning;
                    try self.addErrorDetailsWithCodePage(details);
                    continue;
                },
                error.CodePagePragmaInvalidCodePage => {
                    var details = self.lexer.getErrorDetails(err);
                    if (!self.options.warn_instead_of_error_on_invalid_code_page) {
                        return self.addErrorDetailsWithCodePageAndFail(details);
                    }
                    details.type = .warning;
                    try self.addErrorDetailsWithCodePage(details);
                    continue;
                },
                error.InvalidDigitCharacterInNumberLiteral => {
                    const details = self.lexer.getErrorDetails(err);
                    try self.addErrorDetailsWithCodePage(details);
                    return self.addErrorDetailsWithCodePageAndFail(.{
                        .err = details.err,
                        .type = .note,
                        .code_page = self.lexer.current_code_page,
                        .token = details.token,
                        .print_source_line = false,
                    });
                },
                else => return self.addErrorDetailsWithCodePageAndFail(self.lexer.getErrorDetails(err)),
            };
            break :token token;
        };
        // After every token, set the input code page for its line
        try self.state.input_code_page_lookup.setForToken(self.state.token, self.lexer.current_code_page);
        // But only set the output code page to the current code page if we are past the first code_page pragma in the file.
        // Otherwise, we want to fill the lookup using the default code page so that lookups still work for lines that
        // don't have an explicit output code page set.
        const is_disjoint_code_page = self.options.disjoint_code_page and self.lexer.seen_pragma_code_pages == 1;
        const output_code_page = if (is_disjoint_code_page)
            self.state.output_code_page_lookup.default_code_page
        else
            self.lexer.current_code_page;

        if (is_disjoint_code_page and !self.state.warned_about_disjoint_code_page) {
            try self.addErrorDetailsWithCodePage(.{
                .err = .disjoint_code_page,
                .type = .warning,
                .code_page = self.state.input_code_page_lookup.getForLineNum(self.lexer.last_pragma_code_page_token.?.line_number),
                .token = self.lexer.last_pragma_code_page_token.?,
            });
            try self.addErrorDetailsWithCodePage(.{
                .err = .disjoint_code_page,
                .type = .note,
                .code_page = self.state.input_code_page_lookup.getForLineNum(self.lexer.last_pragma_code_page_token.?.line_number),
                .token = self.lexer.last_pragma_code_page_token.?,
                .print_source_line = false,
            });
            self.state.warned_about_disjoint_code_page = true;
        }

        try self.state.output_code_page_lookup.setForToken(self.state.token, output_code_page);
    }

    fn lookaheadToken(self: *Self, comptime method: Lexer.LexMethod) Error!Token {
        self.state.lookahead_lexer = self.lexer.*;
        return token: while (true) {
            break :token self.state.lookahead_lexer.next(method) catch |err| switch (err) {
                // Ignore this error and get the next valid token, we'll deal with this
                // properly when getting the token for real
                error.CodePagePragmaInIncludedFile => continue,
                else => return self.addErrorDetailsWithCodePageAndFail(self.state.lookahead_lexer.getErrorDetails(err)),
            };
        };
    }

    fn tokenSlice(self: *Self) []const u8 {
        return self.state.token.slice(self.lexer.buffer);
    }

    /// Check that the current token is something that can be used as an ID
    fn checkId(self: *Self) !void {
        switch (self.state.token.id) {
            .literal => {},
            else => {
                return self.addErrorDetailsAndFail(.{
                    .err = .expected_token,
                    .token = self.state.token,
                    .extra = .{ .expected = .literal },
                });
            },
        }
    }

    fn check(self: *Self, expected_token_id: Token.Id) !void {
        if (self.state.token.id != expected_token_id) {
            return self.addErrorDetailsAndFail(.{
                .err = .expected_token,
                .token = self.state.token,
                .extra = .{ .expected = expected_token_id },
            });
        }
    }

    fn checkResource(self: *Self) !ResourceType {
        switch (self.state.token.id) {
            .literal => return ResourceType.fromString(.{
                .slice = self.state.token.slice(self.lexer.buffer),
                .code_page = self.lexer.current_code_page,
            }),
            else => {
                return self.addErrorDetailsAndFail(.{
                    .err = .expected_token,
                    .token = self.state.token,
                    .extra = .{ .expected = .literal },
                });
            },
        }
    }
};
