const std = @import("std");
const mem = std.mem;
const assert = @import("../quirks.zig").inlineAssert;
const Allocator = mem.Allocator;
const ArenaAllocator = std.heap.ArenaAllocator;
const diags = @import("diagnostics.zig");
const internal_os = @import("../os/main.zig");
const Diagnostic = diags.Diagnostic;
const DiagnosticList = diags.DiagnosticList;
const CommaSplitter = @import("CommaSplitter.zig");

const log = std.log.scoped(.cli);

// TODO:
//   - Only `--long=value` format is accepted. Do we want to allow
//     `--long value`? Not currently allowed.

// For trimming
pub const whitespace = " \t";

/// The base errors for arg parsing. Additional errors can be returned due
/// to type-specific parsing but these are always possible.
pub const Error = error{
    ValueRequired,
    InvalidField,
    InvalidValue,
};

/// Parse the command line arguments from iter into dst.
///
/// dst must be a struct. The fields and their types will be used to determine
/// the valid CLI flags. See the tests in this file as an example. For field
/// types that are structs, the struct can implement the `parseCLI` function
/// to do custom parsing.
///
/// If the destination type has a field "_arena" of type `?ArenaAllocator`,
/// an arena allocator will be created (or reused if set already) for any
/// allocations. Allocations are necessary for certain types, like `[]const u8`.
///
/// If the destination type has a field "_diagnostics", it must be of type
/// "DiagnosticList" and any diagnostic messages will be added to that list.
/// When diagnostics are present, only allocation errors will be returned.
///
/// If the destination type has a decl "compatibility", it must be of type
/// std.StaticStringMap(CompatibilityHandler(T)), and it will be used to
/// handle backwards compatibility for fields with the given name. The
/// field name doesn't need to exist (so you can setup compatibility for
/// removed fields). The value is a function that will be called when
/// all other parsing fails for that field. If a field changes such that
/// the old values would NOT error, then the caller should handle that
/// downstream after parsing is done, not through this method.
///
/// Note: If the arena is already non-null, then it will be used. In this
/// case, in the case of an error some memory might be leaked into the arena.
pub fn parse(
    comptime T: type,
    alloc: Allocator,
    dst: *T,
    iter: anytype,
) !void {
    const info = @typeInfo(T);
    assert(info == .@"struct");

    // Make an arena for all our allocations if we support it. Otherwise,
    // use an allocator that always fails. If the arena is already set on
    // the config, then we reuse that. See memory note in parse docs.
    const arena_available = @hasField(T, "_arena");
    var arena_owned: bool = false;
    const arena_alloc = if (arena_available) arena: {
        // If the arena is unset, we create it. We mark that we own it
        // only so that we can clean it up on error.
        if (dst._arena == null) {
            dst._arena = .init(alloc);
            arena_owned = true;
        }

        break :arena dst._arena.?.allocator();
    } else fail: {
        // Note: this is... not safe...
        var fail = std.testing.FailingAllocator.init(alloc, .{});
        break :fail fail.allocator();
    };
    errdefer if (arena_available and arena_owned) {
        dst._arena.?.deinit();
        dst._arena = null;
    };

    while (iter.next()) |arg| {
        // Do manual parsing if we have a hook for it.
        if (@hasDecl(T, "parseManuallyHook")) {
            if (!try dst.parseManuallyHook(
                arena_alloc,
                arg,
                iter,
            )) return;
        }

        // If the destination supports help then we check for it, call
        // the help function and return.
        if (@hasDecl(T, "help")) {
            if (mem.eql(u8, arg, "--help") or
                mem.eql(u8, arg, "-h"))
            {
                try dst.help();
                return;
            }
        }

        // If this doesn't start with "--" then it isn't a config
        // flag. We don't support positional arguments or configuration
        // values set with spaces so this is an error.
        if (!mem.startsWith(u8, arg, "--")) {
            if (comptime !canTrackDiags(T)) return Error.InvalidField;

            // Add our diagnostic
            try dst._diagnostics.append(arena_alloc, .{
                .key = try arena_alloc.dupeZ(u8, arg),
                .message = "invalid field",
                .location = try diags.Location.fromIter(iter, arena_alloc),
            });

            continue;
        }

        var key: []const u8 = arg[2..];
        const value: ?[]const u8 = value: {
            // If the arg has "=" then the value is after the "=".
            if (mem.indexOf(u8, key, "=")) |idx| {
                defer key = key[0..idx];
                break :value key[idx + 1 ..];
            }

            break :value null;
        };

        parseIntoField(T, arena_alloc, dst, key, value) catch |err| err: {
            // If we get an error parsing a field, then we try to fall
            // back to compatibility handlers if able.
            if (@hasDecl(T, "compatibility")) {
                // If we have a compatibility handler for this key, then
                // we call it and see if it handles the error.
                if (T.compatibility.get(key)) |handler| {
                    if (handler(dst, arena_alloc, key, value)) {
                        log.info(
                            "compatibility handler for {s} handled error, you may be using a deprecated field: {}",
                            .{ key, err },
                        );
                        break :err;
                    }
                }
            }

            if (comptime !canTrackDiags(T)) return err;

            // The error set is dependent on comptime T, so we always add
            // an extra error so we can have the "else" below.
            const ErrSet = @TypeOf(err) || error{ Unknown, OutOfMemory } || Error;
            const message: [:0]const u8 = switch (@as(ErrSet, @errorCast(err))) {
                // OOM is not recoverable since we need to allocate to
                // track more error messages.
                error.OutOfMemory => return err,
                error.InvalidField => "unknown field",
                error.ValueRequired => formatValueRequired(T, arena_alloc, key) catch "value required",
                error.InvalidValue => formatInvalidValue(T, arena_alloc, key, value) catch "invalid value",
                else => try std.fmt.allocPrintSentinel(
                    arena_alloc,
                    "unknown error {}",
                    .{err},
                    0,
                ),
            };

            // Add our diagnostic
            try dst._diagnostics.append(arena_alloc, .{
                .key = try arena_alloc.dupeZ(u8, key),
                .message = message,
                .location = try diags.Location.fromIter(iter, arena_alloc),
            });
        };
    }
}

/// The function type for a compatibility handler. The compatibility
/// handler is documented in the `parse` function documentation.
///
/// The function type should return bool if the compatibility was
/// handled, and false otherwise. If false is returned then the
/// naturally occurring error will continue to be processed as if
/// this compatibility handler was not present.
///
/// Compatibility handlers aren't allowed to return errors because
/// they're generally only called in error cases, so we already have
/// an error message to show users. If there is an error in handling
/// the compatibility, then the handler should return false.
pub fn CompatibilityHandler(comptime T: type) type {
    return *const fn (
        dst: *T,
        alloc: Allocator,
        key: []const u8,
        value: ?[]const u8,
    ) bool;
}

/// Convenience function to create a compatibility handler that
/// renames a field from `from` to `to`.
pub fn compatibilityRenamed(
    comptime T: type,
    comptime to: []const u8,
) CompatibilityHandler(T) {
    comptime assert(@hasField(T, to));

    return (struct {
        fn compat(
            dst: *T,
            alloc: Allocator,
            key: []const u8,
            value: ?[]const u8,
        ) bool {
            _ = key;

            parseIntoField(T, alloc, dst, to, value) catch |err| {
                log.warn("error parsing renamed field {s}: {}", .{
                    to,
                    err,
                });

                return false;
            };

            return true;
        }
    }).compat;
}

fn formatValueRequired(
    comptime T: type,
    arena_alloc: std.mem.Allocator,
    key: []const u8,
) std.Io.Writer.Error![:0]const u8 {
    var stream: std.Io.Writer.Allocating = .init(arena_alloc);
    const writer = &stream.writer;

    try writer.print("value required", .{});
    try formatValues(T, key, writer);
    try writer.writeByte(0);

    const written = stream.written();
    return written[0 .. written.len - 1 :0];
}

fn formatInvalidValue(
    comptime T: type,
    arena_alloc: std.mem.Allocator,
    key: []const u8,
    value: ?[]const u8,
) std.Io.Writer.Error![:0]const u8 {
    var stream: std.Io.Writer.Allocating = .init(arena_alloc);
    const writer = &stream.writer;

    try writer.print("invalid value \"{?s}\"", .{value});
    try formatValues(T, key, writer);
    try writer.writeByte(0);

    const written = stream.written();
    return written[0 .. written.len - 1 :0];
}

fn formatValues(
    comptime T: type,
    key: []const u8,
    writer: *std.Io.Writer,
) std.Io.Writer.Error!void {
    @setEvalBranchQuota(2000);
    const typeinfo = @typeInfo(T);
    inline for (typeinfo.@"struct".fields) |f| {
        if (std.mem.eql(u8, key, f.name)) {
            switch (@typeInfo(f.type)) {
                .@"enum" => |e| {
                    try writer.print(", valid values are: ", .{});
                    inline for (e.fields, 0..) |field, i| {
                        if (i != 0) try writer.print(", ", .{});
                        try writer.print("{s}", .{field.name});
                    }
                },
                else => {},
            }
            break;
        }
    }
}

/// Returns true if this type can track diagnostics.
fn canTrackDiags(comptime T: type) bool {
    return @hasField(T, "_diagnostics");
}

/// Parse a single key/value pair into the destination type T.
///
/// This may result in allocations. The allocations can only be freed by freeing
/// all the memory associated with alloc. It is expected that alloc points to
/// an arena.
pub fn parseIntoField(
    comptime T: type,
    alloc: Allocator,
    dst: *T,
    key: []const u8,
    value: ?[]const u8,
) !void {
    const info = @typeInfo(T);
    assert(info == .@"struct");

    inline for (info.@"struct".fields) |field| {
        if (field.name[0] != '_' and mem.eql(u8, field.name, key)) {
            // For optional fields, we just treat it as the child type.
            // This lets optional fields default to null but get set by
            // the CLI.
            const Field = switch (@typeInfo(field.type)) {
                .optional => |opt| opt.child,
                else => field.type,
            };
            const fieldInfo = @typeInfo(Field);
            const canHaveDecls = fieldInfo == .@"struct" or
                fieldInfo == .@"union" or
                fieldInfo == .@"enum";

            // If the value is empty string (set but empty string),
            // then we reset the value to the default.
            if (value) |v| default: {
                if (v.len != 0) break :default;
                // Set default value if possible.
                if (canHaveDecls and @hasDecl(Field, "init")) {
                    try @field(dst, field.name).init(alloc);
                    return;
                }
                const raw = field.default_value_ptr orelse break :default;
                const ptr: *const field.type = @ptrCast(@alignCast(raw));
                @field(dst, field.name) = ptr.*;
                return;
            }

            // If we are a type that can have decls and have a parseCLI decl,
            // we call that and use that to set the value.
            if (canHaveDecls) {
                if (@hasDecl(Field, "parseCLI")) {
                    const fnInfo = @typeInfo(@TypeOf(Field.parseCLI)).@"fn";
                    switch (fnInfo.params.len) {
                        // 1 arg = (input) => output
                        1 => @field(dst, field.name) = try Field.parseCLI(value),

                        // 2 arg = (self, input) => void
                        2 => switch (@typeInfo(field.type)) {
                            .@"struct",
                            .@"union",
                            .@"enum",
                            => try @field(dst, field.name).parseCLI(value),

                            // If the field is optional and set, then we use
                            // the pointer value directly into it. If its not
                            // set we need to create a new instance.
                            .optional => if (@field(dst, field.name)) |*v| {
                                try v.parseCLI(value);
                            } else {
                                // Note: you cannot do @field(dst, name) = undefined
                                // because this causes the value to be "null"
                                // in ReleaseFast modes.
                                var tmp: Field = undefined;
                                try tmp.parseCLI(value);
                                @field(dst, field.name) = tmp;
                            },

                            else => @compileError("unexpected field type"),
                        },

                        // 3 arg = (self, alloc, input) => void
                        3 => switch (@typeInfo(field.type)) {
                            .@"struct",
                            .@"union",
                            .@"enum",
                            => try @field(dst, field.name).parseCLI(alloc, value),

                            .optional => if (@field(dst, field.name)) |*v| {
                                try v.parseCLI(alloc, value);
                            } else {
                                var tmp: Field = undefined;
                                try tmp.parseCLI(alloc, value);
                                @field(dst, field.name) = tmp;
                            },

                            else => @compileError("unexpected field type"),
                        },

                        else => @compileError("parseCLI invalid argument count"),
                    }

                    return;
                }
            }

            // No parseCLI, magic the value based on the type
            @field(dst, field.name) = switch (Field) {
                []const u8 => value: {
                    const slice = value orelse return error.ValueRequired;
                    const buf = try alloc.alloc(u8, slice.len);
                    @memcpy(buf, slice);
                    break :value buf;
                },

                [:0]const u8 => value: {
                    const slice = value orelse return error.ValueRequired;
                    const buf = try alloc.allocSentinel(u8, slice.len, 0);
                    @memcpy(buf, slice);
                    buf[slice.len] = 0;
                    break :value buf;
                },

                bool => try parseBool(value orelse "t"),

                inline u8,
                u16,
                u21,
                u32,
                u64,
                usize,
                i8,
                i16,
                i32,
                i64,
                isize,
                => |Int| std.fmt.parseInt(
                    Int,
                    value orelse return error.ValueRequired,
                    0,
                ) catch return error.InvalidValue,

                f32,
                f64,
                => |Float| std.fmt.parseFloat(
                    Float,
                    value orelse return error.ValueRequired,
                ) catch return error.InvalidValue,

                else => switch (fieldInfo) {
                    .@"enum" => std.meta.stringToEnum(
                        Field,
                        value orelse return error.ValueRequired,
                    ) orelse return error.InvalidValue,

                    .@"struct" => try parseStruct(
                        Field,
                        alloc,
                        value orelse return error.ValueRequired,
                    ),

                    .@"union" => try parseTaggedUnion(
                        Field,
                        alloc,
                        value orelse return error.ValueRequired,
                    ),

                    else => @compileError("unsupported field type"),
                },
            };

            return;
        }
    }

    return error.InvalidField;
}

pub fn parseTaggedUnion(comptime T: type, alloc: Allocator, v: []const u8) !T {
    const info = @typeInfo(T).@"union";
    assert(@typeInfo(info.tag_type.?) == .@"enum");

    // Get the union tag that is being set. We support values with no colon
    // if the value is void so its not an error to have no colon.
    const colon_idx = mem.indexOf(u8, v, ":") orelse v.len;
    const tag_str = std.mem.trim(u8, v[0..colon_idx], whitespace);
    const value = if (colon_idx < v.len) v[colon_idx + 1 ..] else "";

    // Find the field in the union that matches the tag.
    inline for (info.fields) |field| {
        if (mem.eql(u8, field.name, tag_str)) {
            // Special case void types where we don't need a value.
            if (field.type == void) {
                if (value.len > 0) return error.InvalidValue;
                return @unionInit(T, field.name, {});
            }

            // We need to create a struct that looks like this union field.
            // This lets us use parseIntoField as if its a dedicated struct.
            const Target = @Type(.{ .@"struct" = .{
                .layout = .auto,
                .fields = &.{.{
                    .name = field.name,
                    .type = field.type,
                    .default_value_ptr = null,
                    .is_comptime = false,
                    .alignment = @alignOf(field.type),
                }},
                .decls = &.{},
                .is_tuple = false,
            } });

            // Parse the value into the struct
            var t: Target = undefined;
            try parseIntoField(Target, alloc, &t, field.name, value);

            // Build our union
            return @unionInit(T, field.name, @field(t, field.name));
        }
    }

    return error.InvalidValue;
}

fn parseStruct(comptime T: type, alloc: Allocator, v: []const u8) !T {
    return switch (@typeInfo(T).@"struct".layout) {
        .auto => parseAutoStruct(T, alloc, v, null),
        .@"packed" => parsePackedStruct(T, v),
        else => @compileError("unsupported struct layout"),
    };
}

pub fn parseAutoStruct(
    comptime T: type,
    alloc: Allocator,
    v: []const u8,
    default_: ?T,
) !T {
    const info = @typeInfo(T).@"struct";
    comptime assert(info.layout == .auto);

    // We start our result as undefined so we don't get an error for required
    // fields. We track required fields below and we validate that we set them
    // all at the bottom of this function (in addition to setting defaults for
    // optionals).
    var result: T = undefined;

    // Keep track of which fields were set so we can error if a required
    // field was not set.
    const FieldSet = std.StaticBitSet(info.fields.len);
    var fields_set: FieldSet = .initEmpty();

    // We split each value by "," allowing for quoting and escaping.
    var iter: CommaSplitter = .init(v);
    loop: while (try iter.next()) |entry| {
        // Find the key/value, trimming whitespace. The value may be quoted
        // which we strip the quotes from.
        const idx = mem.indexOf(u8, entry, ":") orelse return error.InvalidValue;
        const key = std.mem.trim(u8, entry[0..idx], whitespace);

        // used if we need to decode a double-quoted string.
        var buf: std.Io.Writer.Allocating = .init(alloc);
        defer buf.deinit();

        const value = value: {
            const value = std.mem.trim(u8, entry[idx + 1 ..], whitespace);

            // Detect a quoted string.
            if (value.len >= 2 and
                value[0] == '"' and
                value[value.len - 1] == '"')
            {
                // Decode a double-quoted string as a Zig string literal.
                const parsed = try std.zig.string_literal.parseWrite(&buf.writer, value);
                if (parsed == .failure) return error.InvalidValue;
                break :value buf.written();
            }

            break :value value;
        };

        inline for (info.fields, 0..) |field, i| {
            if (std.mem.eql(u8, field.name, key)) {
                try parseIntoField(T, alloc, &result, key, value);
                fields_set.set(i);
                continue :loop;
            }
        }

        // No field matched
        return error.InvalidValue;
    }

    // Ensure all required fields are set
    inline for (info.fields, 0..) |field, i| {
        if (!fields_set.isSet(i)) {
            @field(result, field.name) = default: {
                // If we're given a default value then we inherit those.
                // Otherwise we use the default values as specified by the
                // struct.
                if (default_) |default| {
                    break :default @field(default, field.name);
                } else {
                    const default_ptr = field.default_value_ptr orelse return error.InvalidValue;
                    const typed_ptr: *const field.type = @ptrCast(@alignCast(default_ptr));
                    break :default typed_ptr.*;
                }
            };
        }
    }

    return result;
}

pub fn parsePackedStruct(comptime T: type, v: []const u8) !T {
    const info = @typeInfo(T).@"struct";
    comptime assert(info.layout == .@"packed");

    var result: T = .{};

    // Allow standalone boolean values like "true" and "false" to
    // turn on or off all of the struct's fields.
    bools: {
        const b = parseBool(v) catch break :bools;
        inline for (info.fields) |field| {
            assert(field.type == bool);
            @field(result, field.name) = b;
        }
        return result;
    }

    // We split each value by ","
    var iter = std.mem.splitSequence(u8, v, ",");
    loop: while (iter.next()) |part_raw| {
        // Determine the field we're looking for and the value. If the
        // field is prefixed with "no-" then we set the value to false.
        const part, const value = part: {
            const negation_prefix = "no-";
            const trimmed = std.mem.trim(u8, part_raw, whitespace);
            if (std.mem.startsWith(u8, trimmed, negation_prefix)) {
                break :part .{ trimmed[negation_prefix.len..], false };
            } else {
                break :part .{ trimmed, true };
            }
        };

        inline for (info.fields) |field| {
            assert(field.type == bool);
            if (std.mem.eql(u8, field.name, part)) {
                @field(result, field.name) = value;
                continue :loop;
            }
        }

        // No field matched
        return error.InvalidValue;
    }

    return result;
}

pub fn parseBool(v: []const u8) !bool {
    const t = &[_][]const u8{ "1", "t", "T", "true" };
    const f = &[_][]const u8{ "0", "f", "F", "false" };

    inline for (t) |str| {
        if (mem.eql(u8, v, str)) return true;
    }
    inline for (f) |str| {
        if (mem.eql(u8, v, str)) return false;
    }

    return error.InvalidValue;
}

test "parse: simple" {
    const testing = std.testing;

    var data: struct {
        a: []const u8 = "",
        b: bool = false,
        @"b-f": bool = true,

        _arena: ?ArenaAllocator = null,
    } = .{};
    defer if (data._arena) |arena| arena.deinit();

    var iter = try std.process.ArgIteratorGeneral(.{}).init(
        testing.allocator,
        "--a=42 --b --b-f=false",
    );
    defer iter.deinit();
    try parse(@TypeOf(data), testing.allocator, &data, &iter);
    try testing.expect(data._arena != null);
    try testing.expectEqualStrings("42", data.a);
    try testing.expect(data.b);
    try testing.expect(!data.@"b-f");

    // Reparsing works
    var iter2 = try std.process.ArgIteratorGeneral(.{}).init(
        testing.allocator,
        "--a=84",
    );
    defer iter2.deinit();
    try parse(@TypeOf(data), testing.allocator, &data, &iter2);
    try testing.expect(data._arena != null);
    try testing.expectEqualStrings("84", data.a);
    try testing.expect(data.b);
    try testing.expect(!data.@"b-f");
}

test "parse: quoted value" {
    const testing = std.testing;

    var data: struct {
        a: u8 = 0,
        b: []const u8 = "",
        _arena: ?ArenaAllocator = null,
    } = .{};
    defer if (data._arena) |arena| arena.deinit();

    var iter = try std.process.ArgIteratorGeneral(.{}).init(
        testing.allocator,
        "--a=\"42\" --b=\"hello!\"",
    );
    defer iter.deinit();
    try parse(@TypeOf(data), testing.allocator, &data, &iter);
    try testing.expectEqual(@as(u8, 42), data.a);
    try testing.expectEqualStrings("hello!", data.b);
}

test "parse: empty value resets to default" {
    const testing = std.testing;

    var data: struct {
        a: u8 = 42,
        b: bool = false,
        _arena: ?ArenaAllocator = null,
    } = .{};
    defer if (data._arena) |arena| arena.deinit();

    var iter = try std.process.ArgIteratorGeneral(.{}).init(
        testing.allocator,
        "--a= --b=",
    );
    defer iter.deinit();
    try parse(@TypeOf(data), testing.allocator, &data, &iter);
    try testing.expectEqual(@as(u8, 42), data.a);
    try testing.expect(!data.b);
}

test "parse: positional arguments are invalid" {
    const testing = std.testing;

    var data: struct {
        a: u8 = 42,
        _arena: ?ArenaAllocator = null,
    } = .{};
    defer if (data._arena) |arena| arena.deinit();

    var iter = try std.process.ArgIteratorGeneral(.{}).init(
        testing.allocator,
        "--a=84 what",
    );
    defer iter.deinit();
    try testing.expectError(
        error.InvalidField,
        parse(@TypeOf(data), testing.allocator, &data, &iter),
    );
    try testing.expectEqual(@as(u8, 84), data.a);
}

test "parse: diagnostic tracking" {
    const testing = std.testing;

    var data: struct {
        a: []const u8 = "",
        b: enum { one } = .one,

        _arena: ?ArenaAllocator = null,
        _diagnostics: DiagnosticList = .{},
    } = .{};
    defer if (data._arena) |arena| arena.deinit();

    var iter = try std.process.ArgIteratorGeneral(.{}).init(
        testing.allocator,
        "--what --a=42",
    );
    defer iter.deinit();
    try parse(@TypeOf(data), testing.allocator, &data, &iter);
    try testing.expect(data._arena != null);
    try testing.expectEqualStrings("42", data.a);
    try testing.expect(data._diagnostics.items().len == 1);
    {
        const diag = data._diagnostics.items()[0];
        try testing.expectEqual(diags.Location.none, diag.location);
        try testing.expectEqualStrings("what", diag.key);
        try testing.expectEqualStrings("unknown field", diag.message);
    }
}

test "parse: diagnostic location" {
    const testing = std.testing;

    var data: struct {
        a: []const u8 = "",
        b: enum { one, two } = .one,

        _arena: ?ArenaAllocator = null,
        _diagnostics: DiagnosticList = .{},
    } = .{};
    defer if (data._arena) |arena| arena.deinit();

    var r: std.Io.Reader = .fixed(
        \\a=42
        \\what
        \\b=two
    );

    var iter: LineIterator = .{ .r = &r, .filepath = "test" };
    try parse(@TypeOf(data), testing.allocator, &data, &iter);
    try testing.expect(data._arena != null);
    try testing.expectEqualStrings("42", data.a);
    try testing.expect(data.b == .two);
    try testing.expect(data._diagnostics.items().len == 1);
    {
        const diag = data._diagnostics.items()[0];
        try testing.expectEqualStrings("what", diag.key);
        try testing.expectEqualStrings("unknown field", diag.message);
        try testing.expectEqualStrings("test", diag.location.file.path);
        try testing.expectEqual(2, diag.location.file.line);
    }
}

test "parse: compatibility handler" {
    const testing = std.testing;

    var data: struct {
        a: bool = false,
        _arena: ?ArenaAllocator = null,

        pub const compatibility: std.StaticStringMap(
            CompatibilityHandler(@This()),
        ) = .initComptime(&.{
            .{ "a", compat },
        });

        fn compat(
            self: *@This(),
            alloc: Allocator,
            key: []const u8,
            value: ?[]const u8,
        ) bool {
            _ = alloc;
            if (std.mem.eql(u8, key, "a")) {
                if (value) |v| {
                    if (mem.eql(u8, v, "yuh")) {
                        self.a = true;
                        return true;
                    }
                }
            }

            return false;
        }
    } = .{};
    defer if (data._arena) |arena| arena.deinit();

    var iter = try std.process.ArgIteratorGeneral(.{}).init(
        testing.allocator,
        "--a=yuh",
    );
    defer iter.deinit();
    try parse(@TypeOf(data), testing.allocator, &data, &iter);
    try testing.expect(data._arena != null);
    try testing.expect(data.a);
}

test "parse: compatibility renamed" {
    const testing = std.testing;

    var data: struct {
        a: bool = false,
        b: bool = false,
        _arena: ?ArenaAllocator = null,

        pub const compatibility: std.StaticStringMap(
            CompatibilityHandler(@This()),
        ) = .initComptime(&.{
            .{ "old", compatibilityRenamed(@This(), "a") },
        });
    } = .{};
    defer if (data._arena) |arena| arena.deinit();

    var iter = try std.process.ArgIteratorGeneral(.{}).init(
        testing.allocator,
        "--old=true --b=true",
    );
    defer iter.deinit();
    try parse(@TypeOf(data), testing.allocator, &data, &iter);
    try testing.expect(data._arena != null);
    try testing.expect(data.a);
    try testing.expect(data.b);
}

test "parseIntoField: ignore underscore-prefixed fields" {
    const testing = std.testing;
    var arena = ArenaAllocator.init(testing.allocator);
    defer arena.deinit();
    const alloc = arena.allocator();

    var data: struct {
        _a: []const u8 = "12",
    } = .{};

    try testing.expectError(
        error.InvalidField,
        parseIntoField(@TypeOf(data), alloc, &data, "_a", "42"),
    );
    try testing.expectEqualStrings("12", data._a);
}

test "parseIntoField: struct with init func" {
    const testing = std.testing;
    var arena = ArenaAllocator.init(testing.allocator);
    defer arena.deinit();
    const alloc = arena.allocator();

    var data: struct {
        a: struct {
            const Self = @This();

            v: []const u8,

            pub fn init(self: *Self, _alloc: Allocator) !void {
                _ = _alloc;
                self.* = .{ .v = "HELLO!" };
            }
        },
    } = undefined;

    try parseIntoField(@TypeOf(data), alloc, &data, "a", "");
    try testing.expectEqual(@as([]const u8, "HELLO!"), data.a.v);
}

test "parseIntoField: string" {
    const testing = std.testing;
    var arena = ArenaAllocator.init(testing.allocator);
    defer arena.deinit();
    const alloc = arena.allocator();

    var data: struct {
        a: []const u8,
    } = undefined;

    try parseIntoField(@TypeOf(data), alloc, &data, "a", "42");
    try testing.expectEqualStrings("42", data.a);
}

test "parseIntoField: sentinel string" {
    const testing = std.testing;
    var arena = ArenaAllocator.init(testing.allocator);
    defer arena.deinit();
    const alloc = arena.allocator();

    var data: struct {
        a: [:0]const u8,
    } = undefined;

    try parseIntoField(@TypeOf(data), alloc, &data, "a", "42");
    try testing.expectEqualStrings("42", data.a);
    try testing.expectEqual(@as(u8, 0), data.a[data.a.len]);
}

test "parseIntoField: bool" {
    const testing = std.testing;
    var arena = ArenaAllocator.init(testing.allocator);
    defer arena.deinit();
    const alloc = arena.allocator();

    var data: struct {
        a: bool,
    } = undefined;

    // True
    try parseIntoField(@TypeOf(data), alloc, &data, "a", "1");
    try testing.expectEqual(true, data.a);
    try parseIntoField(@TypeOf(data), alloc, &data, "a", "t");
    try testing.expectEqual(true, data.a);
    try parseIntoField(@TypeOf(data), alloc, &data, "a", "T");
    try testing.expectEqual(true, data.a);
    try parseIntoField(@TypeOf(data), alloc, &data, "a", "true");
    try testing.expectEqual(true, data.a);

    // False
    try parseIntoField(@TypeOf(data), alloc, &data, "a", "0");
    try testing.expectEqual(false, data.a);
    try parseIntoField(@TypeOf(data), alloc, &data, "a", "f");
    try testing.expectEqual(false, data.a);
    try parseIntoField(@TypeOf(data), alloc, &data, "a", "F");
    try testing.expectEqual(false, data.a);
    try parseIntoField(@TypeOf(data), alloc, &data, "a", "false");
    try testing.expectEqual(false, data.a);
}

test "parseIntoField: unsigned numbers" {
    const testing = std.testing;
    var arena = ArenaAllocator.init(testing.allocator);
    defer arena.deinit();
    const alloc = arena.allocator();

    var data: struct {
        u8: u8,
    } = undefined;

    try parseIntoField(@TypeOf(data), alloc, &data, "u8", "1");
    try testing.expectEqual(@as(u8, 1), data.u8);
}

test "parseIntoField: floats" {
    const testing = std.testing;
    var arena = ArenaAllocator.init(testing.allocator);
    defer arena.deinit();
    const alloc = arena.allocator();

    var data: struct {
        f64: f64,
    } = undefined;

    try parseIntoField(@TypeOf(data), alloc, &data, "f64", "1");
    try testing.expectEqual(@as(f64, 1.0), data.f64);
}

test "parseIntoField: enums" {
    const testing = std.testing;
    var arena = ArenaAllocator.init(testing.allocator);
    defer arena.deinit();
    const alloc = arena.allocator();

    const Enum = enum { one, two, three };
    var data: struct {
        v: Enum,
    } = undefined;

    try parseIntoField(@TypeOf(data), alloc, &data, "v", "two");
    try testing.expectEqual(Enum.two, data.v);
}

test "parseIntoField: packed struct" {
    const testing = std.testing;
    var arena = ArenaAllocator.init(testing.allocator);
    defer arena.deinit();
    const alloc = arena.allocator();

    const Field = packed struct {
        a: bool = false,
        b: bool = true,
    };
    var data: struct {
        v: Field,
    } = undefined;

    try parseIntoField(@TypeOf(data), alloc, &data, "v", "b");
    try testing.expect(!data.v.a);
    try testing.expect(data.v.b);
}

test "parseIntoField: packed struct negation" {
    const testing = std.testing;
    var arena = ArenaAllocator.init(testing.allocator);
    defer arena.deinit();
    const alloc = arena.allocator();

    const Field = packed struct {
        a: bool = false,
        b: bool = true,
    };
    var data: struct {
        v: Field,
    } = undefined;

    try parseIntoField(@TypeOf(data), alloc, &data, "v", "a,no-b");
    try testing.expect(data.v.a);
    try testing.expect(!data.v.b);
}

test "parseIntoField: packed struct true/false" {
    const testing = std.testing;
    var arena = ArenaAllocator.init(testing.allocator);
    defer arena.deinit();
    const alloc = arena.allocator();

    const Field = packed struct {
        a: bool = false,
        b: bool = true,
    };
    var data: struct {
        v: Field,
    } = undefined;

    try parseIntoField(@TypeOf(data), alloc, &data, "v", "true");
    try testing.expect(data.v.a);
    try testing.expect(data.v.b);

    try parseIntoField(@TypeOf(data), alloc, &data, "v", "false");
    try testing.expect(!data.v.a);
    try testing.expect(!data.v.b);

    try testing.expectError(
        error.InvalidValue,
        parseIntoField(@TypeOf(data), alloc, &data, "v", "true,a"),
    );
}

test "parseIntoField: packed struct whitespace" {
    const testing = std.testing;
    var arena = ArenaAllocator.init(testing.allocator);
    defer arena.deinit();
    const alloc = arena.allocator();

    const Field = packed struct {
        a: bool = false,
        b: bool = true,
    };
    var data: struct {
        v: Field,
    } = undefined;

    try parseIntoField(@TypeOf(data), alloc, &data, "v", " a, no-b ");
    try testing.expect(data.v.a);
    try testing.expect(!data.v.b);
}

test "parseIntoField: optional field" {
    const testing = std.testing;
    var arena = ArenaAllocator.init(testing.allocator);
    defer arena.deinit();
    const alloc = arena.allocator();

    var data: struct {
        a: ?bool = null,
    } = .{};

    // True
    try parseIntoField(@TypeOf(data), alloc, &data, "a", "1");
    try testing.expectEqual(true, data.a.?);

    // Unset
    try parseIntoField(@TypeOf(data), alloc, &data, "a", "");
    try testing.expect(data.a == null);
}

test "parseIntoField: struct with parse func" {
    const testing = std.testing;
    var arena = ArenaAllocator.init(testing.allocator);
    defer arena.deinit();
    const alloc = arena.allocator();

    var data: struct {
        a: struct {
            const Self = @This();

            v: []const u8,

            pub fn parseCLI(value: ?[]const u8) !Self {
                _ = value;
                return Self{ .v = "HELLO!" };
            }
        },
    } = undefined;

    try parseIntoField(@TypeOf(data), alloc, &data, "a", "42");
    try testing.expectEqual(@as([]const u8, "HELLO!"), data.a.v);
}

test "parseIntoField: optional struct with parse func" {
    const testing = std.testing;
    var arena = ArenaAllocator.init(testing.allocator);
    defer arena.deinit();
    const alloc = arena.allocator();

    var data: struct {
        a: ?struct {
            const Self = @This();

            v: []const u8,

            pub fn parseCLI(self: *Self, _: Allocator, value: ?[]const u8) !void {
                _ = value;
                self.* = .{ .v = "HELLO!" };
            }
        } = null,
    } = .{};

    try parseIntoField(@TypeOf(data), alloc, &data, "a", "42");
    try testing.expectEqual(@as([]const u8, "HELLO!"), data.a.?.v);
}

test "parseIntoField: struct with basic fields" {
    const testing = std.testing;
    var arena = ArenaAllocator.init(testing.allocator);
    defer arena.deinit();
    const alloc = arena.allocator();

    var data: struct {
        value: struct {
            a: []const u8,
            b: u32,
            c: u8 = 12,
        } = undefined,
    } = .{};

    // Set required fields
    try parseIntoField(@TypeOf(data), alloc, &data, "value", "a:hello,b:42");
    try testing.expectEqualStrings("hello", data.value.a);
    try testing.expectEqual(42, data.value.b);
    try testing.expectEqual(12, data.value.c);

    // Set all fields
    try parseIntoField(@TypeOf(data), alloc, &data, "value", "a:world,b:84,c:24");
    try testing.expectEqualStrings("world", data.value.a);
    try testing.expectEqual(84, data.value.b);
    try testing.expectEqual(24, data.value.c);

    // Missing require dfield
    try testing.expectError(
        error.InvalidValue,
        parseIntoField(@TypeOf(data), alloc, &data, "value", "a:hello"),
    );
}

test "parseIntoField: tagged union" {
    const testing = std.testing;
    var arena = ArenaAllocator.init(testing.allocator);
    defer arena.deinit();
    const alloc = arena.allocator();

    var data: struct {
        value: union(enum) {
            a: u8,
            b: u8,
            c: void,
            d: []const u8,
            e: [:0]const u8,
        } = undefined,
    } = .{};

    // Set one field
    try parseIntoField(@TypeOf(data), alloc, &data, "value", "a:1");
    try testing.expectEqual(1, data.value.a);

    // Set another
    try parseIntoField(@TypeOf(data), alloc, &data, "value", "b:2");
    try testing.expectEqual(2, data.value.b);

    // Set void field
    try parseIntoField(@TypeOf(data), alloc, &data, "value", "c");
    try testing.expectEqual({}, data.value.c);

    // Set string field
    try parseIntoField(@TypeOf(data), alloc, &data, "value", "d:hello");
    try testing.expectEqualStrings("hello", data.value.d);

    // Set sentinel string field
    try parseIntoField(@TypeOf(data), alloc, &data, "value", "e:hello");
    try testing.expectEqualStrings("hello", data.value.e);
}

test "parseIntoField: tagged union unknown filed" {
    const testing = std.testing;
    var arena = ArenaAllocator.init(testing.allocator);
    defer arena.deinit();
    const alloc = arena.allocator();

    var data: struct {
        value: union(enum) {
            a: u8,
            b: u8,
        } = undefined,
    } = .{};

    try testing.expectError(
        error.InvalidValue,
        parseIntoField(@TypeOf(data), alloc, &data, "value", "c:1"),
    );
}

test "parseIntoField: tagged union invalid field value" {
    const testing = std.testing;
    var arena = ArenaAllocator.init(testing.allocator);
    defer arena.deinit();
    const alloc = arena.allocator();

    var data: struct {
        value: union(enum) {
            a: u8,
            b: u8,
        } = undefined,
    } = .{};

    try testing.expectError(
        error.InvalidValue,
        parseIntoField(@TypeOf(data), alloc, &data, "value", "a:hello"),
    );
}

test "parseIntoField: tagged union missing tag" {
    const testing = std.testing;
    var arena = ArenaAllocator.init(testing.allocator);
    defer arena.deinit();
    const alloc = arena.allocator();

    var data: struct {
        value: union(enum) {
            a: u8,
            b: u8,
        } = undefined,
    } = .{};

    try testing.expectError(
        error.InvalidValue,
        parseIntoField(@TypeOf(data), alloc, &data, "value", "a"),
    );
    try testing.expectError(
        error.InvalidValue,
        parseIntoField(@TypeOf(data), alloc, &data, "value", ":a"),
    );
}

/// An iterator that considers its location to be CLI args. It
/// iterates through an underlying iterator and increments a counter
/// to track the current CLI arg index.
///
/// This also ignores any argument that starts with `+`. It assumes that
/// actions were parsed out before this iterator was created.
pub fn ArgsIterator(comptime Iterator: type) type {
    return struct {
        const Self = @This();

        /// The underlying args iterator.
        iterator: Iterator,

        /// Our current index into the iterator. This is 1-indexed.
        /// The 0 value is used to indicate that we haven't read any
        /// values yet.
        index: usize = 0,

        pub fn deinit(self: *Self) void {
            if (@hasDecl(Iterator, "deinit")) {
                self.iterator.deinit();
            }
        }

        pub fn next(self: *Self) ?[]const u8 {
            const value = self.iterator.next() orelse return null;
            self.index += 1;

            // We ignore any argument that starts with "+". This is used
            // to indicate actions and are expected to be parsed out before
            // this iterator is created.
            if (value.len > 0 and value[0] == '+') return self.next();

            return value;
        }

        /// Returns a location for a diagnostic message.
        pub fn location(self: *const Self, _: Allocator) error{}!?diags.Location {
            return .{ .cli = self.index };
        }
    };
}

/// Create an args iterator for the process args. This will skip argv0.
pub fn argsIterator(alloc_gpa: Allocator) internal_os.args.ArgIterator.InitError!ArgsIterator(internal_os.args.ArgIterator) {
    var iter = try internal_os.args.iterator(alloc_gpa);
    errdefer iter.deinit();
    _ = iter.next(); // skip argv0
    return .{ .iterator = iter };
}

test "ArgsIterator" {
    const testing = std.testing;

    const child = try std.process.ArgIteratorGeneral(.{}).init(
        testing.allocator,
        "--what +list-things --a=42",
    );
    const Iter = ArgsIterator(@TypeOf(child));
    var iter: Iter = .{ .iterator = child };
    defer iter.deinit();

    try testing.expectEqualStrings("--what", iter.next().?);
    try testing.expectEqualStrings("--a=42", iter.next().?);
    try testing.expectEqual(@as(?[]const u8, null), iter.next());
    try testing.expectEqual(@as(?[]const u8, null), iter.next());
}

/// Returns an iterator (implements "next") that reads CLI args by line.
/// Each CLI arg is expected to be a single line. This is used to implement
/// configuration files.
pub const LineIterator = struct {
    const Self = @This();

    /// The maximum size a single line can be. We don't expect any
    /// CLI arg to exceed this size. Can't wait to git blame this in
    /// like 4 years and be wrong about this.
    pub const MAX_LINE_SIZE = 4096;

    /// Our stateful reader.
    r: *std.Io.Reader,

    /// Filepath that is used for diagnostics. This is only used for
    /// diagnostic messages so it can be formatted however you want.
    /// It is prefixed to the messages followed by the line number.
    filepath: []const u8 = "",

    /// The current line that we're on. This is 1-indexed because
    /// lines are generally 1-indexed in the real world. The value
    /// can be zero if we haven't read any lines yet.
    line: usize = 0,

    /// This is the buffer where we store the current entry that
    /// is formatted to be compatible with the parse function.
    entry: [MAX_LINE_SIZE]u8 = [_]u8{ '-', '-' } ++ ([_]u8{0} ** (MAX_LINE_SIZE - 2)),

    pub fn init(reader: *std.Io.Reader) Self {
        return .{ .r = reader };
    }

    pub fn next(self: *Self) ?[]const u8 {
        // First prime the reader.
        // File readers at least are initialized with a size of 0,
        // and this will actually prompt the reader to get the actual
        // size of the file, which will be used in the EOF check below.
        //
        // This will also optimize reads down the line as we're
        // more likely to beworking with buffered data.
        //
        // fillMore asserts that the buffer has available capacity,
        // so skip this if it's full.
        if (self.r.bufferedLen() < self.r.buffer.len) {
            self.r.fillMore() catch {};
        }

        var writer: std.Io.Writer = .fixed(self.entry[2..]);

        var entry = while (self.r.seek != self.r.end) {
            // Reset write head
            writer.end = 0;

            _ = self.r.streamDelimiterEnding(&writer, '\n') catch |e| {
                log.warn("cannot read from \"{s}\": {}", .{ self.filepath, e });
                return null;
            };
            _ = self.r.discardDelimiterInclusive('\n') catch {};

            var entry = writer.buffered();
            self.line += 1;

            // Trim any whitespace (including CR) around it
            const trim = std.mem.trim(u8, entry, whitespace ++ "\r");
            if (trim.len != entry.len) {
                std.mem.copyForwards(u8, entry, trim);
                entry = entry[0..trim.len];
            }

            // Ignore blank lines and comments
            if (entry.len == 0 or entry[0] == '#') continue;
            break entry;
        } else return null;

        if (mem.indexOf(u8, entry, "=")) |idx| {
            const key = std.mem.trim(u8, entry[0..idx], whitespace);
            const value = value: {
                var value = std.mem.trim(u8, entry[idx + 1 ..], whitespace);

                // Detect a quoted string.
                if (value.len >= 2 and
                    value[0] == '"' and
                    value[value.len - 1] == '"')
                {
                    // Trim quotes since our CLI args processor expects
                    // quotes to already be gone.
                    value = value[1 .. value.len - 1];
                }

                break :value value;
            };

            const len = key.len + value.len + 1;
            if (entry.len != len) {
                std.mem.copyForwards(u8, entry, key);
                entry[key.len] = '=';
                std.mem.copyForwards(u8, entry[key.len + 1 ..], value);
                entry = entry[0..len];
            }
        }

        // We need to reslice so that we include our '--' at the beginning
        // of our buffer so that we can trick the CLI parser to treat it
        // as CLI args.
        return self.entry[0 .. entry.len + 2];
    }

    /// Returns a location for a diagnostic message.
    pub fn location(
        self: *const Self,
        alloc: Allocator,
    ) Allocator.Error!?diags.Location {
        // If we have no filepath then we have no location.
        if (self.filepath.len == 0) return null;

        return .{ .file = .{
            .path = try alloc.dupe(u8, self.filepath),
            .line = self.line,
        } };
    }
};

/// An iterator valid for arg parsing from a slice.
pub const SliceIterator = struct {
    const Self = @This();

    slice: []const []const u8,
    idx: usize = 0,

    pub fn next(self: *Self) ?[]const u8 {
        if (self.idx >= self.slice.len) return null;
        defer self.idx += 1;
        return self.slice[self.idx];
    }
};

/// Construct a SliceIterator from a slice.
pub fn sliceIterator(slice: []const []const u8) SliceIterator {
    return .{ .slice = slice };
}

test "LineIterator" {
    const testing = std.testing;
    var reader: std.Io.Reader = .fixed(
        \\A
        \\B=42
        \\C
        \\
        \\# A comment
        \\D
        \\
        \\  # An indented comment
        \\  E
        \\
        \\# A quoted string with whitespace
        \\F=  "value "
    );

    var iter: LineIterator = .init(&reader);
    try testing.expectEqualStrings("--A", iter.next().?);
    try testing.expectEqualStrings("--B=42", iter.next().?);
    try testing.expectEqualStrings("--C", iter.next().?);
    try testing.expectEqualStrings("--D", iter.next().?);
    try testing.expectEqualStrings("--E", iter.next().?);
    try testing.expectEqualStrings("--F=value ", iter.next().?);
    try testing.expectEqual(@as(?[]const u8, null), iter.next());
    try testing.expectEqual(@as(?[]const u8, null), iter.next());
}

test "LineIterator end in newline" {
    const testing = std.testing;
    var reader: std.Io.Reader = .fixed("A\n\n");

    var iter: LineIterator = .init(&reader);
    try testing.expectEqualStrings("--A", iter.next().?);
    try testing.expectEqual(@as(?[]const u8, null), iter.next());
    try testing.expectEqual(@as(?[]const u8, null), iter.next());
}

test "LineIterator spaces around '='" {
    const testing = std.testing;
    var reader: std.Io.Reader = .fixed("A = B\n\n");

    var iter: LineIterator = .init(&reader);
    try testing.expectEqualStrings("--A=B", iter.next().?);
    try testing.expectEqual(@as(?[]const u8, null), iter.next());
    try testing.expectEqual(@as(?[]const u8, null), iter.next());
}

test "LineIterator no value" {
    const testing = std.testing;
    var reader: std.Io.Reader = .fixed("A = \n\n");

    var iter: LineIterator = .init(&reader);
    try testing.expectEqualStrings("--A=", iter.next().?);
    try testing.expectEqual(@as(?[]const u8, null), iter.next());
}

test "LineIterator with CRLF line endings" {
    const testing = std.testing;
    var reader: std.Io.Reader = .fixed("A\r\nB = C\r\n");

    var iter: LineIterator = .init(&reader);
    try testing.expectEqualStrings("--A", iter.next().?);
    try testing.expectEqualStrings("--B=C", iter.next().?);
    try testing.expectEqual(@as(?[]const u8, null), iter.next());
    try testing.expectEqual(@as(?[]const u8, null), iter.next());
}

test "LineIterator with buffered reader" {
    const testing = std.testing;
    var f: std.Io.Reader = .fixed("A\nB = C\n");
    var buf: [2]u8 = undefined;
    var r = f.limited(.unlimited, &buf);
    const reader = &r.interface;

    var iter: LineIterator = .init(reader);
    try testing.expectEqualStrings("--A", iter.next().?);
    try testing.expectEqualStrings("--B=C", iter.next().?);
    try testing.expectEqual(@as(?[]const u8, null), iter.next());
    try testing.expectEqual(@as(?[]const u8, null), iter.next());
}

test "LineIterator with buffered and primed reader" {
    const testing = std.testing;
    var f: std.Io.Reader = .fixed("A\nB = C\n");
    var buf: [2]u8 = undefined;
    var r = f.limited(.unlimited, &buf);
    const reader = &r.interface;

    try reader.fill(buf.len);

    var iter: LineIterator = .init(reader);
    try testing.expectEqualStrings("--A", iter.next().?);
    try testing.expectEqualStrings("--B=C", iter.next().?);
    try testing.expectEqual(@as(?[]const u8, null), iter.next());
    try testing.expectEqual(@as(?[]const u8, null), iter.next());
}
