// Zero allocation argument parsing for unix-like systems.
// Released under the Zero Clause BSD (0BSD) license:
//
// Copyright 2022 Isaac Freund
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

const std = @import("std");
const cstr = std.cstr;

pub const Flag = struct {
    name: [*:0]const u8,
    kind: enum { boolean, arg },
};

pub fn ParseResult(comptime flags: []const Flag) type {
    return struct {
        const Self = @This();

        const FlagData = struct {
            name: [*:0]const u8,
            value: union {
                boolean: bool,
                arg: ?[*:0]const u8,
            },
        };

        /// Remaining args after the recognized flags
        args: []const [*:0]const u8,
        /// Data obtained from parsed flags
        flag_data: [flags.len]FlagData = blk: {
            // Init all flags to false/null
            var flag_data: [flags.len]FlagData = undefined;
            inline for (flags, &flag_data) |flag, *flag_data_i| {
                flag_data_i.* = switch (flag.kind) {
                    .boolean => .{
                        .name = flag.name,
                        .value = .{ .boolean = false },
                    },
                    .arg => .{
                        .name = flag.name,
                        .value = .{ .arg = null },
                    },
                };
            }
            break :blk flag_data;
        },

        pub fn boolFlag(self: Self, flag_name: [*:0]const u8) bool {
            for (self.flag_data) |flag_data| {
                if (cstr.cmp(flag_data.name, flag_name) == 0) return flag_data.value.boolean;
            }
            unreachable; // Invalid flag_name
        }

        pub fn argFlag(self: Self, flag_name: [*:0]const u8) ?[:0]const u8 {
            for (self.flag_data) |flag_data| {
                if (cstr.cmp(flag_data.name, flag_name) == 0) {
                    return std.mem.span(flag_data.value.arg);
                }
            }
            unreachable; // Invalid flag_name
        }
    };
}

pub fn parse(args: []const [*:0]const u8, comptime flags: []const Flag) !ParseResult(flags) {
    var ret: ParseResult(flags) = .{ .args = undefined };

    var arg_idx: usize = 0;
    while (arg_idx < args.len) : (arg_idx += 1) {
        var parsed_flag = false;
        inline for (flags, &ret.flag_data) |flag, *flag_data_i| {
            if (cstr.cmp(flag.name, args[arg_idx]) == 0) {
                switch (flag.kind) {
                    .boolean => flag_data_i.*.value.boolean = true,
                    .arg => {
                        arg_idx += 1;
                        if (arg_idx == args.len) {
                            std.log.err("option '" ++ flag.name ++
                                "' requires an argument but none was provided!", .{});
                            return error.MissingFlagArgument;
                        }
                        flag_data_i.*.value.arg = args[arg_idx];
                    },
                }
                parsed_flag = true;
            }
        }
        if (!parsed_flag) break;
    }

    ret.args = args[arg_idx..];

    return ret;
}
