const std = @import("std");
const builtin = @import("builtin");

const os = std.os;
const io = std.io;
const mem = std.mem;
const Allocator = mem.Allocator;
const ArrayList = std.ArrayList;
const Buffer = std.Buffer;

const arg = @import("arg.zig");
const c = @import("c.zig");
const introspect = @import("introspect.zig");
const Args = arg.Args;
const Flag = arg.Flag;
const Module = @import("module.zig").Module;
const Target = @import("target.zig").Target;

var stderr: &io.OutStream(io.FileOutStream.Error) = undefined;
var stdout: &io.OutStream(io.FileOutStream.Error) = undefined;

const usage =
    \\usage: zig [command] [options]
    \\
    \\Commands:
    \\
    \\  build                        Build project from build.zig
    \\  build-exe   [source]         Create executable from source or object files
    \\  build-lib   [source]         Create library from source or object files
    \\  build-obj   [source]         Create object from source or assembly
    \\  fmt         [source]         Parse file and render in canonical zig format
    \\  run         [source]         Create executable and run immediately
    \\  targets                      List available compilation targets
    \\  test        [source]         Create and run a test build
    \\  translate-c [source]         Convert c code to zig code
    \\  version                      Print version number and exit
    \\  zen                          Print zen of zig and exit
    \\
    \\
    ;

const Command = struct {
    name: []const u8,
    exec: fn(&Allocator, []const []const u8) error!void,
};

pub fn main() !void {
    var allocator = std.heap.c_allocator;

    var stdout_file = try std.io.getStdOut();
    var stdout_out_stream = std.io.FileOutStream.init(&stdout_file);
    stdout = &stdout_out_stream.stream;

    var stderr_file = try std.io.getStdErr();
    var stderr_out_stream = std.io.FileOutStream.init(&stderr_file);
    stderr = &stderr_out_stream.stream;

    const args = try os.argsAlloc(allocator);
    defer os.argsFree(allocator, args);

    if (args.len <= 1) {
        try stderr.write(usage);
        os.exit(1);
    }

    const commands = []Command {
        Command { .name = "build",       .exec = cmdBuild      },
        Command { .name = "build-exe",   .exec = cmdBuildExe   },
        Command { .name = "build-lib",   .exec = cmdBuildLib   },
        Command { .name = "build-obj",   .exec = cmdBuildObj   },
        Command { .name = "fmt",         .exec = cmdFmt        },
        Command { .name = "run",         .exec = cmdRun        },
        Command { .name = "targets",     .exec = cmdTargets    },
        Command { .name = "test",        .exec = cmdTest       },
        Command { .name = "translate-c", .exec = cmdTranslateC },
        Command { .name = "version",     .exec = cmdVersion    },
        Command { .name = "zen",         .exec = cmdZen        },

        // undocumented commands
        Command { .name = "help",        .exec = cmdHelp       },
        Command { .name = "internal",    .exec = cmdInternal   },
    };

    for (commands) |command| {
        if (mem.eql(u8, command.name, args[1])) {
            try command.exec(allocator, args[2..]);
            return;
        }
    }

    try stderr.print("unknown command: {}\n\n", args[1]);
    try stderr.write(usage);
}

// cmd:build ///////////////////////////////////////////////////////////////////////////////////////

const usage_build =
    \\usage: zig build <options>
    \\
    \\General Options:
    \\   --help                       Print this help and exit
    \\   --init                       Generate a build.zig template
    \\   --build-file [file]          Override path to build.zig
    \\   --cache-dir [path]           Override path to cache directory
    \\   --verbose                    Print commands before executing them
    \\   --prefix [path]              Override default install prefix
    \\
    \\Project-Specific Options:
    \\
    \\   Project-specific options become available when the build file is found.
    \\
    \\Advanced Options:
    \\   --build-file [file]          Override path to build.zig
    \\   --cache-dir [path]           Override path to cache directory
    \\   --verbose-tokenize           Enable compiler debug output for tokenization
    \\   --verbose-ast                Enable compiler debug output for parsing into an AST
    \\   --verbose-link               Enable compiler debug output for linking
    \\   --verbose-ir                 Enable compiler debug output for Zig IR
    \\   --verbose-llvm-ir            Enable compiler debug output for LLVM IR
    \\   --verbose-cimport            Enable compiler debug output for C imports
    \\
    \\
    ;

const args_build_spec = []Flag {
    Flag.Bool("--help"),
    Flag.Bool("--init"),
    Flag.Arg1("--build-file"),
    Flag.Arg1("--cache-dir"),
    Flag.Bool("--verbose"),
    Flag.Arg1("--prefix"),

    Flag.Arg1("--build-file"),
    Flag.Arg1("--cache-dir"),
    Flag.Bool("--verbose-tokenize"),
    Flag.Bool("--verbose-ast"),
    Flag.Bool("--verbose-link"),
    Flag.Bool("--verbose-ir"),
    Flag.Bool("--verbose-llvm-ir"),
    Flag.Bool("--verbose-cimport"),
};

const missing_build_file =
    \\No 'build.zig' file found.
    \\
    \\Initialize a 'build.zig' template file with `zig build --init`,
    \\or build an executable directly with `zig build-exe $FILENAME.zig`.
    \\
    \\See: `zig build --help` or `zig help` for more options.
    \\
    ;

fn cmdBuild(allocator: &Allocator, args: []const []const u8) !void {
    var flags = try Args.parse(allocator, args_build_spec, args);
    defer flags.deinit();

    if (flags.present("help")) {
        try stderr.write(usage_build);
        os.exit(0);
    }

    const zig_lib_dir = try introspect.resolveZigLibDir(allocator);
    defer allocator.free(zig_lib_dir);

    const zig_std_dir = try os.path.join(allocator, zig_lib_dir, "std");
    defer allocator.free(zig_std_dir);

    const special_dir = try os.path.join(allocator, zig_std_dir, "special");
    defer allocator.free(special_dir);

    const build_runner_path = try os.path.join(allocator, special_dir, "build_runner.zig");
    defer allocator.free(build_runner_path);

    const build_file = flags.single("build-file") ?? "build.zig";
    const build_file_abs = try os.path.resolve(allocator, ".", build_file);
    defer allocator.free(build_file_abs);

    const build_file_exists = os.File.access(allocator, build_file_abs, os.default_file_mode) catch false;

    if (flags.present("init")) {
        if (build_file_exists) {
            try stderr.print("build.zig already exists\n");
            os.exit(1);
        }

        // need a new scope for proper defer scope finalization on exit
        {
            const build_template_path = try os.path.join(allocator, special_dir, "build_file_template.zig");
            defer allocator.free(build_template_path);

            try os.copyFile(allocator, build_template_path, build_file_abs);
            try stderr.print("wrote build.zig template\n");
        }

        os.exit(0);
    }

    if (!build_file_exists) {
        try stderr.write(missing_build_file);
        os.exit(1);
    }

    // TODO: Invoke build.zig entrypoint directly?
    var zig_exe_path = try os.selfExePath(allocator);
    defer allocator.free(zig_exe_path);

    var build_args = ArrayList([]const u8).init(allocator);
    defer build_args.deinit();

    const build_file_basename = os.path.basename(build_file_abs);
    const build_file_dirname = os.path.dirname(build_file_abs);

    var full_cache_dir: []u8 = undefined;
    if (flags.single("cache-dir")) |cache_dir| {
        full_cache_dir = try os.path.resolve(allocator, ".", cache_dir, full_cache_dir);
    } else {
        full_cache_dir = try os.path.join(allocator, build_file_dirname, "zig-cache");
    }
    defer allocator.free(full_cache_dir);

    const path_to_build_exe = try os.path.join(allocator, full_cache_dir, "build");
    defer allocator.free(path_to_build_exe);

    try build_args.append(path_to_build_exe);
    try build_args.append(zig_exe_path);
    try build_args.append(build_file_dirname);
    try build_args.append(full_cache_dir);

    var proc = try os.ChildProcess.init(build_args.toSliceConst(), allocator);
    defer proc.deinit();

    var term = try proc.spawnAndWait();
    switch (term) {
        os.ChildProcess.Term.Exited => |status| {
            if (status != 0) {
                try stderr.print("{} exited with status {}\n", build_args.at(0), status);
                os.exit(1);
            }
        },
        os.ChildProcess.Term.Signal => |signal| {
            try stderr.print("{} killed by signal {}\n", build_args.at(0), signal);
            os.exit(1);
        },
        os.ChildProcess.Term.Stopped => |signal| {
            try stderr.print("{} stopped by signal {}\n", build_args.at(0), signal);
            os.exit(1);
        },
        os.ChildProcess.Term.Unknown => |status| {
            try stderr.print("{} encountered unknown failure {}\n", build_args.at(0), status);
            os.exit(1);
        },
    }
}

// cmd:build-exe ///////////////////////////////////////////////////////////////////////////////////

const usage_build_generic =
    \\usage: zig build-exe <options> [file]
    \\       zig build-lib <options> [file]
    \\       zig build-obj <options> [file]
    \\
    \\General Options:
    \\  --help                       Print this help and exit
    \\  --color [auto|off|on]        Enable or disable colored error messages
    \\
    \\Compile Options:
    \\  --assembly [source]          Add assembly file to build
    \\  --cache-dir [path]           Override the cache directory
    \\  --emit [filetype]            Emit a specific file format as compilation output
    \\  --enable-timing-info         Print timing diagnostics
    \\  --libc-include-dir [path]    Directory where libc stdlib.h resides
    \\  --name [name]                Override output name
    \\  --output [file]              Override destination path
    \\  --output-h [file]            Override generated header file path
    \\  --pkg-begin [name] [path]    Make package available to import and push current pkg
    \\  --pkg-end                    Pop current pkg
    \\  --release-fast               Build with optimizations on and safety off
    \\  --release-safe               Build with optimizations on and safety on
    \\  --static                     Output will be statically linked
    \\  --strip                      Exclude debug symbols
    \\  --target-arch [name]         Specify target architecture
    \\  --target-environ [name]      Specify target environment
    \\  --target-os [name]           Specify target operating system
    \\  --verbose-tokenize           Turn on compiler debug output for tokenization
    \\  --verbose-ast-tree           Turn on compiler debug output for parsing into an AST (tree view)
    \\  --verbose-ast-fmt            Turn on compiler debug output for parsing into an AST (render source)
    \\  --verbose-link               Turn on compiler debug output for linking
    \\  --verbose-ir                 Turn on compiler debug output for Zig IR
    \\  --verbose-llvm-ir            Turn on compiler debug output for LLVM IR
    \\  --verbose-cimport            Turn on compiler debug output for C imports
    \\  -dirafter [dir]              Same as -isystem but do it last
    \\  -isystem [dir]               Add additional search path for other .h files
    \\  -mllvm [arg]                 Additional arguments to forward to LLVM's option processing
    \\
    \\Link Options:
    \\  --ar-path [path]             Set the path to ar
    \\  --dynamic-linker [path]      Set the path to ld.so
    \\  --each-lib-rpath             Add rpath for each used dynamic library
    \\  --libc-lib-dir [path]        Directory where libc crt1.o resides
    \\  --libc-static-lib-dir [path] Directory where libc crtbegin.o resides
    \\  --msvc-lib-dir [path]        (windows) directory where vcruntime.lib resides
    \\  --kernel32-lib-dir [path]    (windows) directory where kernel32.lib resides
    \\  --library [lib]              Link against lib
    \\  --forbid-library [lib]       Make it an error to link against lib
    \\  --library-path [dir]         Add a directory to the library search path
    \\  --linker-script [path]       Use a custom linker script
    \\  --object [obj]               Add object file to build
    \\  -rdynamic                    Add all symbols to the dynamic symbol table
    \\  -rpath [path]                Add directory to the runtime library search path
    \\  -mconsole                    (windows) --subsystem console to the linker
    \\  -mwindows                    (windows) --subsystem windows to the linker
    \\  -framework [name]            (darwin) link against framework
    \\  -mios-version-min [ver]      (darwin) set iOS deployment target
    \\  -mmacosx-version-min [ver]   (darwin) set Mac OS X deployment target
    \\  --ver-major [ver]            Dynamic library semver major version
    \\  --ver-minor [ver]            Dynamic library semver minor version
    \\  --ver-patch [ver]            Dynamic library semver patch version
    \\
    \\
    ;

const args_build_generic = []Flag {
    Flag.Bool("--help"),
    Flag.Option("--color", []const []const u8 { "auto", "off", "on" }),

    Flag.ArgMergeN("--assembly", 1),
    Flag.Arg1("--cache-dir"),
    Flag.Option("--emit", []const []const u8 { "asm", "bin", "llvm-ir" }),
    Flag.Bool("--enable-timing-info"),
    Flag.Arg1("--libc-include-dir"),
    Flag.Arg1("--name"),
    Flag.Arg1("--output"),
    Flag.Arg1("--output-h"),
    // NOTE: Parsed manually after initial check
    Flag.ArgN("--pkg-begin", 2),
    Flag.Bool("--pkg-end"),
    Flag.Bool("--release-fast"),
    Flag.Bool("--release-safe"),
    Flag.Bool("--static"),
    Flag.Bool("--strip"),
    Flag.Arg1("--target-arch"),
    Flag.Arg1("--target-environ"),
    Flag.Arg1("--target-os"),
    Flag.Bool("--verbose-tokenize"),
    Flag.Bool("--verbose-ast-tree"),
    Flag.Bool("--verbose-ast-fmt"),
    Flag.Bool("--verbose-link"),
    Flag.Bool("--verbose-ir"),
    Flag.Bool("--verbose-llvm-ir"),
    Flag.Bool("--verbose-cimport"),
    Flag.Arg1("-dirafter"),
    Flag.ArgMergeN("-isystem", 1),
    Flag.Arg1("-mllvm"),

    Flag.Arg1("--ar-path"),
    Flag.Arg1("--dynamic-linker"),
    Flag.Bool("--each-lib-rpath"),
    Flag.Arg1("--libc-lib-dir"),
    Flag.Arg1("--libc-static-lib-dir"),
    Flag.Arg1("--msvc-lib-dir"),
    Flag.Arg1("--kernel32-lib-dir"),
    Flag.ArgMergeN("--library", 1),
    Flag.ArgMergeN("--forbid-library", 1),
    Flag.ArgMergeN("--library-path", 1),
    Flag.Arg1("--linker-script"),
    Flag.ArgMergeN("--object", 1),
    // NOTE: Removed -L since it would need to be special-cased and we have an alias in library-path
    Flag.Bool("-rdynamic"),
    Flag.Arg1("-rpath"),
    Flag.Bool("-mconsole"),
    Flag.Bool("-mwindows"),
    Flag.ArgMergeN("-framework", 1),
    Flag.Arg1("-mios-version-min"),
    Flag.Arg1("-mmacosx-version-min"),
    Flag.Arg1("--ver-major"),
    Flag.Arg1("--ver-minor"),
    Flag.Arg1("--ver-patch"),
};

fn buildOutputType(allocator: &Allocator, args: []const []const u8, out_type: Module.Kind) !void {
    var flags = try Args.parse(allocator, args_build_generic, args);
    defer flags.deinit();

    if (flags.present("help")) {
        try stderr.write(usage_build_generic);
        os.exit(0);
    }

    var build_mode = builtin.Mode.Debug;
    if (flags.present("release-fast")) {
        build_mode = builtin.Mode.ReleaseFast;
    } else if (flags.present("release-safe")) {
        build_mode = builtin.Mode.ReleaseSafe;
    }

    var color = Module.ErrColor.Auto;
    if (flags.single("color")) |color_flag| {
        if (mem.eql(u8, color_flag, "auto")) {
            color = Module.ErrColor.Auto;
        } else if (mem.eql(u8, color_flag, "on")) {
            color = Module.ErrColor.On;
        } else if (mem.eql(u8, color_flag, "off")) {
            color = Module.ErrColor.Off;
        } else {
            unreachable;
        }
    }

    var emit_type = Module.Emit.Binary;
    if (flags.single("emit")) |emit_flag| {
        if (mem.eql(u8, emit_flag, "asm")) {
            emit_type = Module.Emit.Assembly;
        } else if (mem.eql(u8, emit_flag, "bin")) {
            emit_type = Module.Emit.Binary;
        } else if (mem.eql(u8, emit_flag, "llvm-ir")) {
            emit_type = Module.Emit.LlvmIr;
        } else {
            unreachable;
        }
    }

    var cur_pkg = try Module.CliPkg.init(allocator, "", "", null); // TODO: Need a path, name?
    defer cur_pkg.deinit();

    var i: usize = 0;
    while (i < args.len) : (i += 1) {
        const arg_name = args[i];
        if (mem.eql(u8, "--pkg-begin", arg_name)) {
            // following two arguments guaranteed to exist due to arg parsing
            i += 1;
            const new_pkg_name = args[i];
            i += 1;
            const new_pkg_path = args[i];

            var new_cur_pkg = try Module.CliPkg.init(allocator, new_pkg_name, new_pkg_path, cur_pkg);
            try cur_pkg.children.append(new_cur_pkg);
            cur_pkg = new_cur_pkg;
        } else if (mem.eql(u8, "--pkg-end", arg_name)) {
            if (cur_pkg.parent == null) {
                try stderr.print("encountered --pkg-end with no matching --pkg-begin\n");
                os.exit(1);
            }
            cur_pkg = ??cur_pkg.parent;
        }
    }

    if (cur_pkg.parent != null) {
        try stderr.print("unmatched --pkg-begin\n");
        os.exit(1);
    }

    var in_file: ?[]const u8 = undefined;
    switch (flags.positionals.len) {
        0 => {
            try stderr.write("--name [name] not provided and unable to infer\n");
            os.exit(1);
        },
        1 => {
            in_file = flags.positionals.at(0);
        },
        else => {
            try stderr.write("only one zig input file is accepted during build\n");
            os.exit(1);
        },
    }

    const basename = os.path.basename(??in_file);
    var it = mem.split(basename, ".");
    const root_name = it.next() ?? {
        try stderr.write("file name cannot be empty\n");
        os.exit(1);
    };

    const asm_a= flags.many("assembly");
    const obj_a = flags.many("object");
    if (in_file == null and (obj_a == null or (??obj_a).len == 0) and (asm_a == null or (??asm_a).len == 0)) {
        try stderr.write("Expected source file argument or at least one --object or --assembly argument\n");
        os.exit(1);
    }

    if (out_type == Module.Kind.Obj and (obj_a != null and (??obj_a).len != 0)) {
        try stderr.write("When building an object file, --object arguments are invalid\n");
        os.exit(1);
    }

    const zig_root_source_file = in_file;

    const full_cache_dir = os.path.resolve(allocator, ".", flags.single("cache-dir") ?? "zig-cache"[0..]) catch {
        os.exit(1);
    };
    defer allocator.free(full_cache_dir);

    const zig_lib_dir = introspect.resolveZigLibDir(allocator) catch os.exit(1);
    defer allocator.free(zig_lib_dir);

    var module =
        try Module.create(
            allocator,
            root_name,
            zig_root_source_file,
            Target.Native,
            out_type,
            build_mode,
            zig_lib_dir,
            full_cache_dir
        );
    defer module.destroy();

    module.version_major = try std.fmt.parseUnsigned(u32, flags.single("ver-major") ?? "0", 10);
    module.version_minor = try std.fmt.parseUnsigned(u32, flags.single("ver-minor") ?? "0", 10);
    module.version_patch = try std.fmt.parseUnsigned(u32, flags.single("ver-patch") ?? "0", 10);

    module.is_test = false;

    if (flags.single("linker-script")) |linker_script| {
        module.linker_script = linker_script;
    }

    module.each_lib_rpath = flags.present("each-lib-rpath");

    var clang_argv_buf = ArrayList([]const u8).init(allocator);
    defer clang_argv_buf.deinit();
    if (flags.many("mllvm")) |mllvm_flags| {
        for (mllvm_flags) |mllvm| {
            try clang_argv_buf.append("-mllvm");
            try clang_argv_buf.append(mllvm);
        }

        module.llvm_argv = mllvm_flags;
        module.clang_argv = clang_argv_buf.toSliceConst();
    }

    module.strip = flags.present("strip");
    module.is_static = flags.present("static");

    if (flags.single("libc-lib-dir")) |libc_lib_dir| {
        module.libc_lib_dir = libc_lib_dir;
    }
    if (flags.single("libc-static-lib-dir")) |libc_static_lib_dir| {
        module.libc_static_lib_dir = libc_static_lib_dir;
    }
    if (flags.single("libc-include-dir")) |libc_include_dir| {
        module.libc_include_dir = libc_include_dir;
    }
    if (flags.single("msvc-lib-dir")) |msvc_lib_dir| {
        module.msvc_lib_dir = msvc_lib_dir;
    }
    if (flags.single("kernel32-lib-dir")) |kernel32_lib_dir| {
        module.kernel32_lib_dir = kernel32_lib_dir;
    }
    if (flags.single("dynamic-linker")) |dynamic_linker| {
        module.dynamic_linker = dynamic_linker;
    }

    module.verbose_tokenize = flags.present("verbose-tokenize");
    module.verbose_ast_tree = flags.present("verbose-ast-tree");
    module.verbose_ast_fmt = flags.present("verbose-ast-fmt");
    module.verbose_link = flags.present("verbose-link");
    module.verbose_ir = flags.present("verbose-ir");
    module.verbose_llvm_ir = flags.present("verbose-llvm-ir");
    module.verbose_cimport = flags.present("verbose-cimport");

    module.err_color = color;

    if (flags.many("library-path")) |lib_dirs| {
        module.lib_dirs = lib_dirs;
    }

    if (flags.many("framework")) |frameworks| {
        module.darwin_frameworks = frameworks;
    }

    if (flags.many("rpath")) |rpath_list| {
        module.rpath_list = rpath_list;
    }

    if (flags.single("output-h")) |output_h| {
        module.out_h_path = output_h;
    }

    module.windows_subsystem_windows = flags.present("mwindows");
    module.windows_subsystem_console = flags.present("mconsole");
    module.linker_rdynamic = flags.present("rdynamic");

    if (flags.single("mmacosx-version-min") != null and flags.single("mios-version-min") != null) {
        try stderr.write("-mmacosx-version-min and -mios-version-min options not allowed together\n");
        os.exit(1);
    }

    if (flags.single("mmacosx-version-min")) |ver| {
        module.darwin_version_min = Module.DarwinVersionMin { .MacOS = ver };
    }
    if (flags.single("mios-version-min")) |ver| {
        module.darwin_version_min = Module.DarwinVersionMin { .Ios = ver };
    }

    module.emit_file_type = emit_type;
    if (flags.many("object")) |objects| {
        module.link_objects = objects;
    }
    if (flags.many("assembly")) |assembly_files| {
        module.assembly_files = assembly_files;
    }

    try module.build();
    try module.link(flags.single("out-file") ?? null);

    if (flags.present("print-timing-info")) {
        // codegen_print_timing_info(g, stderr);
    }

    try stderr.print("building {}: {}\n", @tagName(out_type), in_file);
}

fn cmdBuildExe(allocator: &Allocator, args: []const []const u8) !void {
    try buildOutputType(allocator, args, Module.Kind.Exe);
}

// cmd:build-lib ///////////////////////////////////////////////////////////////////////////////////

fn cmdBuildLib(allocator: &Allocator, args: []const []const u8) !void {
    try buildOutputType(allocator, args, Module.Kind.Lib);
}

// cmd:build-obj ///////////////////////////////////////////////////////////////////////////////////

fn cmdBuildObj(allocator: &Allocator, args: []const []const u8) !void {
    try buildOutputType(allocator, args, Module.Kind.Obj);
}

// cmd:fmt /////////////////////////////////////////////////////////////////////////////////////////

const usage_fmt =
    \\usage: zig fmt [file]...
    \\
    \\   Formats the input files and modifies them in-place.
    \\
    \\Options:
    \\   --help                 Print this help and exit
    \\   --keep-backups         Retain backup entries for every file
    \\
    \\
    ;

const args_fmt_spec = []Flag {
    Flag.Bool("--help"),
    Flag.Bool("--keep-backups"),
};

fn cmdFmt(allocator: &Allocator, args: []const []const u8) !void {
    var flags = try Args.parse(allocator, args_fmt_spec, args);
    defer flags.deinit();

    if (flags.present("help")) {
        try stderr.write(usage_fmt);
        os.exit(0);
    }

    if (flags.positionals.len == 0) {
        try stderr.write("expected at least one source file argument\n");
        os.exit(1);
    }

    for (flags.positionals.toSliceConst()) |file_path| {
        var file = try os.File.openRead(allocator, file_path);
        defer file.close();

        const source_code = io.readFileAlloc(allocator, file_path) catch |err| {
            try stderr.print("unable to open '{}': {}", file_path, err);
            continue;
        };
        defer allocator.free(source_code);

        var tree = std.zig.parse(allocator, source_code) catch |err| {
            try stderr.print("error parsing file '{}': {}\n", file_path, err);
            continue;
        };
        defer tree.deinit();


        var error_it = tree.errors.iterator(0);
        while (error_it.next()) |parse_error| {
            const token = tree.tokens.at(parse_error.loc());
            const loc = tree.tokenLocation(0, parse_error.loc());
            try stderr.print("{}:{}:{}: error: ", file_path, loc.line + 1, loc.column + 1);
            try tree.renderError(parse_error, stderr);
            try stderr.print("\n{}\n", source_code[loc.line_start..loc.line_end]);
            {
                var i: usize = 0;
                while (i < loc.column) : (i += 1) {
                    try stderr.write(" ");
                }
            }
            {
                const caret_count = token.end - token.start;
                var i: usize = 0;
                while (i < caret_count) : (i += 1) {
                    try stderr.write("~");
                }
            }
            try stderr.write("\n");
        }
        if (tree.errors.len != 0) {
            continue;
        }

        try stderr.print("{}\n", file_path);

        const baf = try io.BufferedAtomicFile.create(allocator, file_path);
        defer baf.destroy();

        try std.zig.render(allocator, baf.stream(), &tree);
        try baf.finish();
    }
}

// cmd:targets /////////////////////////////////////////////////////////////////////////////////////

fn cmdTargets(allocator: &Allocator, args: []const []const u8) !void {
    try stdout.write("Architectures:\n");
    {
        comptime var i: usize = 0;
        inline while (i < @memberCount(builtin.Arch)) : (i += 1) {
            comptime const arch_tag = @memberName(builtin.Arch, i);
            // NOTE: Cannot use empty string, see #918.
            comptime const native_str =
                if (comptime mem.eql(u8, arch_tag, @tagName(builtin.arch))) " (native)\n" else "\n";

            try stdout.print("  {}{}", arch_tag, native_str);
        }
    }
    try stdout.write("\n");

    try stdout.write("Operating Systems:\n");
    {
        comptime var i: usize = 0;
        inline while (i < @memberCount(builtin.Os)) : (i += 1) {
            comptime const os_tag = @memberName(builtin.Os, i);
            // NOTE: Cannot use empty string, see #918.
            comptime const native_str =
                if (comptime mem.eql(u8, os_tag, @tagName(builtin.os))) " (native)\n" else "\n";

            try stdout.print("  {}{}", os_tag, native_str);
        }
    }
    try stdout.write("\n");

    try stdout.write("Environments:\n");
    {
        comptime var i: usize = 0;
        inline while (i < @memberCount(builtin.Environ)) : (i += 1) {
            comptime const environ_tag = @memberName(builtin.Environ, i);
            // NOTE: Cannot use empty string, see #918.
            comptime const native_str =
                if (comptime mem.eql(u8, environ_tag, @tagName(builtin.environ))) " (native)\n" else "\n";

            try stdout.print("  {}{}", environ_tag, native_str);
        }
    }
}

// cmd:version /////////////////////////////////////////////////////////////////////////////////////

fn cmdVersion(allocator: &Allocator, args: []const []const u8) !void {
    try stdout.print("{}\n", std.cstr.toSliceConst(c.ZIG_VERSION_STRING));
}

// cmd:test ////////////////////////////////////////////////////////////////////////////////////////

const usage_test =
    \\usage: zig test [file]...
    \\
    \\Options:
    \\   --help                 Print this help and exit
    \\
    \\
    ;

const args_test_spec = []Flag {
    Flag.Bool("--help"),
};


fn cmdTest(allocator: &Allocator, args: []const []const u8) !void {
    var flags = try Args.parse(allocator, args_build_spec, args);
    defer flags.deinit();

    if (flags.present("help")) {
        try stderr.write(usage_test);
        os.exit(0);
    }

    if (flags.positionals.len != 1) {
        try stderr.write("expected exactly one zig source file\n");
        os.exit(1);
    }

    // compile the test program into the cache and run

    // NOTE: May be overlap with buildOutput, take the shared part out.
    try stderr.print("testing file {}\n", flags.positionals.at(0));
}

// cmd:run /////////////////////////////////////////////////////////////////////////////////////////

// Run should be simple and not expose the full set of arguments provided by build-exe. If specific
// build requirements are need, the user should `build-exe` then `run` manually.
const usage_run =
    \\usage: zig run [file] -- <runtime args>
    \\
    \\Options:
    \\   --help                 Print this help and exit
    \\
    \\
    ;

const args_run_spec = []Flag {
    Flag.Bool("--help"),
};


fn cmdRun(allocator: &Allocator, args: []const []const u8) !void {
    var compile_args = args;
    var runtime_args: []const []const u8 = []const []const u8 {};

    for (args) |argv, i| {
        if (mem.eql(u8, argv, "--")) {
            compile_args = args[0..i];
            runtime_args = args[i+1..];
            break;
        }
    }
    var flags = try Args.parse(allocator, args_run_spec, compile_args);
    defer flags.deinit();

    if (flags.present("help")) {
        try stderr.write(usage_run);
        os.exit(0);
    }

    if (flags.positionals.len != 1) {
        try stderr.write("expected exactly one zig source file\n");
        os.exit(1);
    }

    try stderr.print("runtime args:\n");
    for (runtime_args) |cargs| {
        try stderr.print("{}\n", cargs);
    }
}

// cmd:translate-c /////////////////////////////////////////////////////////////////////////////////

const usage_translate_c =
    \\usage: zig translate-c [file]
    \\
    \\Options:
    \\  --help                       Print this help and exit
    \\  --enable-timing-info         Print timing diagnostics
    \\  --output [path]              Output file to write generated zig file (default: stdout)
    \\
    \\
    ;

const args_translate_c_spec = []Flag {
    Flag.Bool("--help"),
    Flag.Bool("--enable-timing-info"),
    Flag.Arg1("--libc-include-dir"),
    Flag.Arg1("--output"),
};

fn cmdTranslateC(allocator: &Allocator, args: []const []const u8) !void {
    var flags = try Args.parse(allocator, args_translate_c_spec, args);
    defer flags.deinit();

    if (flags.present("help")) {
        try stderr.write(usage_translate_c);
        os.exit(0);
    }

    if (flags.positionals.len != 1) {
        try stderr.write("expected exactly one c source file\n");
        os.exit(1);
    }

    // set up codegen

    const zig_root_source_file = null;

    // NOTE: translate-c shouldn't require setting up the full codegen instance as it does in
    // the C++ compiler.

    // codegen_create(g);
    // codegen_set_out_name(g, null);
    // codegen_translate_c(g, flags.positional.at(0))

    var output_stream = stdout;
    if (flags.single("output")) |output_file| {
        var file = try os.File.openWrite(allocator, output_file);
        defer file.close();

        var file_stream = io.FileOutStream.init(&file);
        // TODO: Not being set correctly, still stdout
        output_stream = &file_stream.stream;
    }

    // ast_render(g, output_stream, g->root_import->root, 4);
    try output_stream.write("pub const example = 10;\n");

    if (flags.present("enable-timing-info")) {
        // codegen_print_timing_info(g, stdout);
        try stderr.write("printing timing info for translate-c\n");
    }
}

// cmd:help ////////////////////////////////////////////////////////////////////////////////////////

fn cmdHelp(allocator: &Allocator, args: []const []const u8) !void {
    try stderr.write(usage);
}

// cmd:zen /////////////////////////////////////////////////////////////////////////////////////////

const info_zen =
    \\
    \\ * Communicate intent precisely.
    \\ * Edge cases matter.
    \\ * Favor reading code over writing code.
    \\ * Only one obvious way to do things.
    \\ * Runtime crashes are better than bugs.
    \\ * Compile errors are better than runtime crashes.
    \\ * Incremental improvements.
    \\ * Avoid local maximums.
    \\ * Reduce the amount one must remember.
    \\ * Minimize energy spent on coding style.
    \\ * Together we serve end users.
    \\
    \\
    ;

fn cmdZen(allocator: &Allocator, args: []const []const u8) !void {
    try stdout.write(info_zen);
}

// cmd:internal ////////////////////////////////////////////////////////////////////////////////////

const usage_internal =
    \\usage: zig internal [subcommand]
    \\
    \\Sub-Commands:
    \\  build-info                   Print static compiler build-info
    \\
    \\
    ;

fn cmdInternal(allocator: &Allocator, args: []const []const u8) !void {
    if (args.len == 0) {
        try stderr.write(usage_internal);
        os.exit(1);
    }

    const sub_commands = []Command {
        Command { .name = "build-info", .exec = cmdInternalBuildInfo },
    };

    for (sub_commands) |sub_command| {
        if (mem.eql(u8, sub_command.name, args[0])) {
            try sub_command.exec(allocator, args[1..]);
            return;
        }
    }

    try stderr.print("unknown sub command: {}\n\n", args[0]);
    try stderr.write(usage_internal);
}

fn cmdInternalBuildInfo(allocator: &Allocator, args: []const []const u8) !void {
    try stdout.print(
        \\ZIG_CMAKE_BINARY_DIR {}
        \\ZIG_CXX_COMPILER     {}
        \\ZIG_LLVM_CONFIG_EXE  {}
        \\ZIG_LLD_INCLUDE_PATH {}
        \\ZIG_LLD_LIBRARIES    {}
        \\ZIG_STD_FILES        {}
        \\ZIG_C_HEADER_FILES   {}
        \\ZIG_DIA_GUIDS_LIB    {}
        \\
        ,
        std.cstr.toSliceConst(c.ZIG_CMAKE_BINARY_DIR),
        std.cstr.toSliceConst(c.ZIG_CXX_COMPILER),
        std.cstr.toSliceConst(c.ZIG_LLVM_CONFIG_EXE),
        std.cstr.toSliceConst(c.ZIG_LLD_INCLUDE_PATH),
        std.cstr.toSliceConst(c.ZIG_LLD_LIBRARIES),
        std.cstr.toSliceConst(c.ZIG_STD_FILES),
        std.cstr.toSliceConst(c.ZIG_C_HEADER_FILES),
        std.cstr.toSliceConst(c.ZIG_DIA_GUIDS_LIB),
    );
}
