const std = #import("std");
const Arena = std.Arena;
const c_slice = std.c_slice;
const byte_equal = std.byte_equal;
const print = std.print;
const print_usize = std.print_usize;
const exit = std.os.exit;

const Parser = struct{
    text: [&]const u8,
    index: u32 = 0,
    length: u32,
    current_line: u32 = 0,
    current_line_offset: u32 = 0,

    const expect_byte = fn(parser: &Parser, byte: u8) void {
        const current_ch = parser.text[parser.index];
        if (current_ch != byte) {
            print("Expected '");
            var a = [1]u8{byte};
            print(a.&);
            print("', got '");
            a[0] = current_ch;
            print(a.&);
            print("'\n");
            exit(1);
        }
    }

    const skip_whitespace = fn (parser: &Parser) void {
        const length = parser.length;

        const pointer = parser.text;
        while (parser.index < length) {
            const ch = pointer[parser.index];
            const new_line = ch == '\n';
            const is_space = ch == ' ' or ch == '\t' or new_line or ch == '\r';
            if (new_line) {
                parser.current_line += 1;
                parser.current_line_offset = parser.index + 1;
            }
            
            if (!is_space) {
                break;
            }

            parser.index += 1;
        }
    }

    const raw_string = fn (parser: &Parser) u32 {
        const start_index = parser.index;
        const text = parser.text;
        var index: u32 = parser.index;
        while (index < parser.length) {
            const ch = text[index];
            switch (ch) {
                'a'...'z', 'A'...'Z', '_' => index += 1,
                else => break,
            }
        }

        parser.index = index;

        return start_index;
    }

    const identifier = fn (parser: &Parser) []const u8 {
        const start_index = parser.raw_string();
        const slice = parser.text[start_index.. parser.index];
        
        // TODO: check if the identifier matches keywords

        return slice;
    }
};

const parse = fn (arena: &Arena, bytes: []const u8) *!void {
    if (bytes.length >= 0xffffffff) {
        unreachable;
    }

    const length: u32 = #cast(bytes.length);
    var parser = Parser{
        .text = bytes.pointer,
        .length = length,
    };

    while (parser.index < length) {
        parser.skip_whitespace();

        const current_index = parser.index;
        if (current_index == length) {
            break;
        }

        const slice = bytes[current_index..];
        const is_const = byte_equal(slice[0.."const".length], "const");
        const is_var = byte_equal(slice[0.."var".length], "var");
        const is_test = byte_equal(slice[0.."test".length], "test");
        const is_comptime = byte_equal(slice[0.."comptime".length], "comptime");

        if (is_const) {
            const space_index: u32 = "const".length;
            const ch = slice[space_index];
            const next_ch = slice[space_index + 1];
            const is_normal_space = (ch == ' ' or ch == '\n') or (ch == '\t' or ch == '\r');
            const is_comment = ch == '/' and next_ch == '/';
            const is_space = is_normal_space or is_comment;
            if (!is_space) {
                exit(1);
            }
            parser.index += space_index;
            parser.skip_whitespace();
            const identifier = parser.identifier();
            parser.skip_whitespace();
            parser.expect_byte('=');
            parser.skip_whitespace();
            exit(0);
        } else if (is_var) {
            const space_index: u32 = "var".length;
            const ch = slice[space_index];
            const next_ch = slice[space_index + 1];
            const is_normal_space = (ch == ' ' or ch == '\n') or (ch == '\t' or ch == '\r');
            const is_comment = ch == '/' and next_ch == '/';
            const is_space = is_normal_space or is_comment;
            if (!is_space) {
                exit(1);
            }
            parser.index += space_index;
            parser.skip_whitespace();
            exit(0);
        } else if (is_test) {
            const space_index: u32 = "test".length;
            const ch = slice[space_index];
            const next_ch = slice[space_index + 1];
            const is_normal_space = (ch == ' ' or ch == '\n') or (ch == '\t' or ch == '\r');
            const is_comment = ch == '/' and next_ch == '/';
            const is_space = is_normal_space or is_comment;
            if (!is_space) {
                exit(1);
            }
            parser.index += space_index;
            parser.skip_whitespace();
            exit(0);
        } else if (is_comptime) {
            const space_index: u32 = "comptime".length;
            const ch = slice[space_index];
            const next_ch = slice[space_index + 1];
            const is_normal_space = (ch == ' ' or ch == '\n') or (ch == '\t' or ch == '\r');
            const is_comment = ch == '/' and next_ch == '/';
            const is_space = is_normal_space or is_comment;
            if (!is_space) {
                exit(1);
            }
            parser.index += space_index;
            parser.skip_whitespace();
            exit(0);
        } else {
            exit(1);
        }
    }
}

const FileStartToken = enum{
    "comptime",
    "test",
    "const",
    "var",
};

const ArgumentProcessingError = error{
    no_arguments,
};

const Token = struct {
    const Id = enum(u8) {
        invalid,
        keyword_unsigned_integer,
        keyword_signed_integer,
        identifier,
    };
};

const FixedKeyword = enum{
    "comptime",
    "const",
    "var",
    "void",
    "noreturn",
    "while",
    "bool",
    "true",
    "false",
    "fn",
    "unreachable",
    "return",
    "ssize",
    "usize",
    ""switch",
    "if",
    "else",
    "struct",
    "enum",
    "null",
    "align",
    "for",
    "undefined",
    "break",
    "test",
    "catch",
    "try",
    "orelse",
    "error",
    "and",
    "or",
    "bitfield",
    "Self",
    "any",
    "type",
    "continue",
};

const get_argument = fn (real_argument: []const u8, wanted_argument: []const u8, command_arguments: []const [&:0]const u8, i_ptr: &usize) ?[]const u8 {
    const i = i_ptr.@;

    const are_equal = byte_equal(real_argument, wanted_argument);
    if (are_equal) {
        if (i < command_arguments.length) {
            const new_i = i + 1;
            const argument = c_slice(command_arguments[new_i]);
            i_ptr.@ = new_i;
            return argument;
        } else {
            print("error: unterminated argument: '");
            print(real_argument);
            print("'\n");
            exit(1);
        }
    } else {
        return null;
    }
}

const command_exe = fn (arena: &Arena, command_arguments: []const [&:0]const u8) *!void { 
    var i: usize = 0;

    var maybe_output_argument: ?[]const u8 = null;
    var maybe_main_source_file: ?[]const u8 = null;
    var maybe_main_executable_name: ?[]const u8 = null;

    while (i < command_arguments.length) {
        const command_argument = c_slice(command_arguments[i]);

        if (get_argument(command_argument, "-o", command_arguments, i.&)) |out_arg| {
            maybe_output_argument = out_arg;
        } else if (get_argument(command_argument, "-main_source_file", command_arguments, i.&)) |src_arg| {
            maybe_main_source_file = src_arg;
        } else {
            print("error: unhandled argument: '");
            print(command_argument);
            print("'\n");
            exit(1);
        }

        i += 1;
    }

    const main_source_file = maybe_main_source_file orelse {
        print("error: no main source file specified\n");
        exit(1);
    };

    const main_executable_name = maybe_main_executable_name orelse (std.os.basename(main_source_file[0..main_source_file.length - 9]) orelse unreachable); // 9 => "/main.nat".length

    const file_descriptor = try std.os.open(#cast(main_source_file.pointer), .{});
    const file_size = try file_descriptor.get_size();
    const file_buffer = try arena.new_array($u8, file_size);
    file_descriptor.read_all(file_buffer);

    parse(arena, file_buffer);
}

const main = fn() *!void {
    const arena = try Arena.init(std.megabytes(64));
    const argument_count = std.start.argument_count;

    if (argument_count <= 1) {
        return ArgumentProcessingError.no_arguments;
    }

    const argument_values = std.start.argument_values;
    const arguments = argument_values[0..argument_count];

    const command = c_slice(arguments[1]);
    const command_arguments = arguments[2..];

    if (byte_equal(command, "build")) {
        print("TODO: build");
    } else if (byte_equal(command, "clang") or byte_equal(command, "-cc1") or byte_equal(command, "-cc1as")) {
        unreachable;
    } else if (byte_equal(command, "cc")) {
        unreachable;
    } else if (byte_equal(command, "c++")) {
        unreachable;
    } else if (byte_equal(command, "exe")) {
        command_exe(arena, command_arguments);
    } else if (byte_equal(command, "lib")) {
        unreachable;
    } else if (byte_equal(command, "obj")) {
        unreachable;
    } else if (byte_equal(command, "test")) {
        print("TODO: test");
    } else {
        unreachable;
    }
}
