const std = @import("std");
const logger = @import("./logger.zig");
const bun = @import("./global.zig");
const string = bun.string;
const Output = bun.Output;
const Global = bun.Global;
const Environment = bun.Environment;
const strings = bun.strings;
const MutableString = bun.MutableString;
const stringZ = bun.stringZ;
const default_allocator = bun.default_allocator;
const CodePoint = bun.CodePoint;
const C = bun.C;
const CodepointIterator = @import("./string_immutable.zig").CodepointIterator;
const Analytics = @import("./analytics/analytics_thread.zig");
const Fs = @import("./fs.zig");
const Api = @import("./api/schema.zig").Api;
const which = @import("./which.zig").which;
const Variable = struct {
    key: string,
    value: string,
    has_nested_value: bool = false,
};

// i don't expect anyone to actually use the escape line feed character
const escLineFeed = 0x0C;
// arbitrary character that is invalid in a real text file
const implicitQuoteCharacter = 8;

// you get 4k. I hope you don't need more than that.
threadlocal var temporary_nested_value_buffer: [4096]u8 = undefined;

pub const Lexer = struct {
    source: *const logger.Source,
    iter: CodepointIterator,
    cursor: CodepointIterator.Cursor = CodepointIterator.Cursor{},
    _codepoint: CodePoint = 0,
    current: usize = 0,
    last_non_space: usize = 0,
    prev_non_space: usize = 0,
    start: usize = 0,
    end: usize = 0,
    has_nested_value: bool = false,
    has_newline_before: bool = true,
    was_quoted: bool = false,

    pub inline fn codepoint(this: *const Lexer) CodePoint {
        return this.cursor.c;
    }

    pub inline fn step(this: *Lexer) void {
        const ended = !this.iter.next(&this.cursor);
        if (ended) this.cursor.c = -1;
        this.current = this.cursor.i;
    }

    pub fn eatNestedValue(
        _: *Lexer,
        comptime ContextType: type,
        ctx: *ContextType,
        comptime Writer: type,
        writer: Writer,
        variable: Variable,
        comptime getter: fn (ctx: *const ContextType, key: string) ?string,
    ) !void {
        var i: usize = 0;
        var last_flush: usize = 0;

        top: while (i < variable.value.len) {
            switch (variable.value[i]) {
                '$' => {
                    i += 1;
                    const start = i;

                    const curly_braces_offset = @as(usize, @boolToInt(variable.value[i] == '{'));
                    i += curly_braces_offset;

                    while (i < variable.value.len) {
                        switch (variable.value[i]) {
                            'a'...'z', 'A'...'Z', '0'...'9', '-', '_' => {
                                i += 1;
                            },
                            '}' => {
                                i += curly_braces_offset;
                                break;
                            },
                            else => {
                                break;
                            },
                        }
                    }

                    try writer.writeAll(variable.value[last_flush .. start - 1]);
                    last_flush = i;
                    const name = variable.value[start + curly_braces_offset .. i - curly_braces_offset];

                    if (@call(.{ .modifier = .always_inline }, getter, .{ ctx, name })) |new_value| {
                        if (new_value.len > 0) {
                            try writer.writeAll(new_value);
                        }
                    }

                    continue :top;
                },
                '\\' => {
                    i += 1;
                    switch (variable.value[i]) {
                        '$' => {
                            i += 1;
                            continue;
                        },
                        else => {},
                    }
                },
                else => {},
            }
            i += 1;
        }

        try writer.writeAll(variable.value[last_flush..]);
    }

    pub fn eatValue(
        lexer: *Lexer,
        comptime quote: CodePoint,
    ) string {
        var was_quoted = false;
        switch (comptime quote) {
            '"', '\'' => {
                lexer.step();
                was_quoted = true;
            },

            else => {},
        }

        var start = lexer.current;
        var last_non_space: usize = start;
        var any_spaces = false;

        while (true) {
            switch (lexer.codepoint()) {
                '\\' => {
                    lexer.step();
                    // Handle Windows CRLF

                    switch (lexer.codepoint()) {
                        '\r' => {
                            lexer.step();
                            if (lexer.codepoint() == '\n') {
                                lexer.step();
                            }
                            continue;
                        },
                        '$' => {
                            lexer.step();
                            continue;
                        },
                        else => {
                            continue;
                        },
                    }
                },
                -1 => {
                    lexer.end = lexer.current;

                    return lexer.source.contents[start..if (any_spaces) @minimum(last_non_space, lexer.source.contents.len) else lexer.source.contents.len];
                },
                '$' => {
                    lexer.has_nested_value = true;
                },

                '#' => {
                    lexer.step();
                    lexer.eatComment();

                    return lexer.source.contents[start .. last_non_space + 1];
                },

                '\n', '\r', escLineFeed => {
                    switch (comptime quote) {
                        '\'' => {
                            lexer.end = lexer.current;
                            lexer.step();
                            return lexer.source.contents[start..@minimum(lexer.end, lexer.source.contents.len)];
                        },
                        implicitQuoteCharacter => {
                            lexer.end = lexer.current;
                            lexer.step();

                            return lexer.source.contents[start..@minimum(if (any_spaces) last_non_space + 1 else lexer.end, lexer.end)];
                        },
                        '"' => {
                            // We keep going
                        },
                        else => {},
                    }
                },
                quote => {
                    lexer.end = lexer.current;
                    lexer.step();

                    lexer.was_quoted = was_quoted;
                    return lexer.source.contents[start..@minimum(
                        lexer.end,
                        lexer.source.contents.len,
                    )];
                },
                ' ' => {
                    any_spaces = true;
                    while (lexer.codepoint() == ' ') lexer.step();
                    continue;
                },
                else => {},
            }
            if (lexer.codepoint() != ' ') last_non_space = lexer.current;
            lexer.step();
        }
        unreachable;
    }

    pub fn eatComment(this: *Lexer) void {
        while (true) {
            switch (this.codepoint()) {
                '\r' => {
                    this.step();
                    if (this.codepoint() == '\n') {
                        return;
                    }
                },
                '\n' => {
                    this.step();
                    return;
                },
                -1 => {
                    return;
                },
                else => {
                    this.step();
                },
            }
        }
    }

    // const NEWLINE = '\n'
    // const RE_INI_KEY_VAL = /^\s*([\w.-]+)\s*=\s*(.*)?\s*$/
    // const RE_NEWLINES = /\\n/g
    // const NEWLINES_MATCH = /\r\n|\n|\r/
    pub fn next(this: *Lexer, comptime is_process_env: bool) ?Variable {
        if (this.end == 0) this.step();

        const start = this.start;

        this.has_newline_before = this.end == 0;

        var last_non_space = start;
        restart: while (true) {
            last_non_space = switch (this.codepoint()) {
                ' ', '\r', '\n' => last_non_space,
                else => this.current,
            };

            switch (this.codepoint()) {
                0, -1 => {
                    return null;
                },
                '#' => {
                    this.step();

                    this.eatComment();
                    continue :restart;
                },
                '\r', '\n', 0x2028, 0x2029 => {
                    this.step();
                    this.has_newline_before = true;
                    continue;
                },

                // Valid keys:
                'a'...'z', 'A'...'Z', '0'...'9', '_', '-', '.' => {
                    this.start = this.current;
                    this.step();
                    var key_end: usize = 0;
                    while (true) {
                        switch (this.codepoint()) {

                            // to match npm's "dotenv" behavior, we ignore lines that don't have an equals
                            '\r', '\n', escLineFeed => {
                                this.end = this.current;
                                this.step();
                                continue :restart;
                            },
                            0, -1 => {
                                this.end = this.current;
                                return if (last_non_space > this.start)
                                    Variable{ .key = this.source.contents[this.start..@minimum(last_non_space + 1, this.source.contents.len)], .value = "" }
                                else
                                    null;
                            },
                            'a'...'z', 'A'...'Z', '0'...'9', '_', '-', '.' => {},
                            '=' => {
                                this.end = this.current;
                                if (key_end > 0) {
                                    this.end = key_end;
                                }
                                const key = this.source.contents[this.start..this.end];
                                if (key.len == 0) return null;
                                this.step();

                                // we don't need to do special parsing on process-level environment variable values
                                // if they're quoted, we should keep them quoted.
                                // https://github.com/oven-sh/bun/issues/40
                                if (comptime is_process_env) {
                                    const current = this.current;
                                    // TODO: remove this loop
                                    // it's not as simple as just setting to the end of the string
                                    while (this.codepoint() != -1) : (this.step()) {}
                                    return Variable{
                                        .key = key,
                                        .value = this.source.contents[current..],
                                        // nested values are unsupported in process environment variables
                                        .has_nested_value = false,
                                    };
                                }

                                this.has_nested_value = false;
                                inner: while (true) {
                                    switch (this.codepoint()) {
                                        '"' => {
                                            const value = this.eatValue('"');
                                            return Variable{
                                                .key = key,
                                                .value = value,
                                                .has_nested_value = this.has_nested_value,
                                            };
                                        },
                                        '\'' => {
                                            const value = this.eatValue('\'');
                                            return Variable{
                                                .key = key,
                                                .value = value,
                                                .has_nested_value = this.has_nested_value,
                                            };
                                        },
                                        0, -1 => {
                                            return Variable{ .key = key, .value = "" };
                                        },
                                        '\r', '\n', escLineFeed => {
                                            this.step();
                                            return Variable{ .key = key, .value = "" };
                                        },
                                        // consume unquoted leading spaces
                                        ' ' => {
                                            this.step();
                                            while (this.codepoint() == ' ') this.step();
                                            continue :inner;
                                        },
                                        // we treat everything else the same as if it were wrapped in single quotes
                                        // except we don't terminate on that character
                                        else => {
                                            const value = this.eatValue(implicitQuoteCharacter);
                                            return Variable{
                                                .key = key,
                                                .value = value,
                                                .has_nested_value = this.has_nested_value,
                                            };
                                        },
                                    }
                                }
                            },
                            ' ' => {
                                // Set key end to the last non space character
                                key_end = this.current;
                                this.step();
                                while (this.codepoint() == ' ') this.step();
                                continue;
                            },
                            else => {},
                        }
                        this.step();
                    }
                },
                else => {},
            }

            this.step();
        }
    }

    pub fn init(source: *const logger.Source) Lexer {
        return Lexer{
            .source = source,
            .iter = CodepointIterator.init(source.contents),
        };
    }
};

pub const Loader = struct {
    map: *Map,
    allocator: std.mem.Allocator,

    @".env.local": ?logger.Source = null,
    @".env.development": ?logger.Source = null,
    @".env.production": ?logger.Source = null,
    @".env": ?logger.Source = null,

    quiet: bool = false,

    did_load_process: bool = false,

    const empty_string_value: string = "\"\"";

    pub fn getNodePath(this: *Loader, fs: *Fs.FileSystem, buf: *Fs.PathBuffer) ?[:0]const u8 {
        if (this.get("NODE") orelse this.get("npm_node_execpath")) |node| {
            @memcpy(buf, node.ptr, node.len);
            buf[node.len] = 0;
            return buf[0..node.len :0];
        }

        if (which(buf, this.map.get("PATH") orelse return null, fs.top_level_dir, "node")) |node| {
            return node;
        }

        return null;
    }

    pub fn isCI(this: *const Loader) bool {
        return (this.map.get("CI") orelse
            this.map.get("TDDIUM") orelse
            this.map.get("JENKINS_URL") orelse
            this.map.get("bamboo.buildKey")) != null;
    }

    pub fn loadNodeJSConfig(this: *Loader, fs: *Fs.FileSystem) !void {
        var buf: Fs.PathBuffer = undefined;

        var node = this.getNodePath(fs, &buf) orelse return;
        var cloned = try fs.dirname_store.append([]const u8, std.mem.span(node));
        try this.map.put("NODE", cloned);
        try this.map.put("npm_node_execpath", cloned);
    }

    pub fn get(this: *const Loader, key: string) ?string {
        var _key = key;
        if (_key.len > 0 and _key[0] == '$') {
            _key = key[1..];
        }

        if (_key.len == 0) return null;

        return this.map.get(_key);
    }

    pub fn getAuto(this: *const Loader, key: string) string {
        return this.get(key) orelse key;
    }

    /// Load values from the environment into Define.
    ///
    /// If there is a framework, values from the framework are inserted with a
    /// **lower priority** so that users may override defaults. Unlike regular
    /// defines, environment variables are loaded as JavaScript string literals.
    ///
    /// Empty enivronment variables become empty strings.
    pub fn copyForDefine(
        this: *Loader,
        comptime JSONStore: type,
        to_json: *JSONStore,
        comptime StringStore: type,
        to_string: *StringStore,
        framework_defaults: Api.StringMap,
        behavior: Api.DotEnvBehavior,
        prefix: string,
        allocator: std.mem.Allocator,
    ) ![]u8 {
        var iter = this.map.iter();
        var key_count: usize = 0;
        var string_map_hashes = try allocator.alloc(u64, framework_defaults.keys.len);
        defer allocator.free(string_map_hashes);
        const invalid_hash = std.math.maxInt(u64) - 1;
        std.mem.set(u64, string_map_hashes, invalid_hash);

        var key_buf: []u8 = "";
        // Frameworks determine an allowlist of values

        for (framework_defaults.keys) |key, i| {
            if (key.len > "process.env.".len and strings.eqlComptime(key[0.."process.env.".len], "process.env.")) {
                const hashable_segment = key["process.env.".len..];
                string_map_hashes[i] = std.hash.Wyhash.hash(0, hashable_segment);
            }
        }

        // We have to copy all the keys to prepend "process.env" :/
        var key_buf_len: usize = 0;
        var e_strings_to_allocate: usize = 0;

        if (behavior != .disable) {
            if (behavior == .prefix) {
                std.debug.assert(prefix.len > 0);

                while (iter.next()) |entry| {
                    if (strings.startsWith(entry.key_ptr.*, prefix)) {
                        key_buf_len += entry.key_ptr.len;
                        key_count += 1;
                        e_strings_to_allocate += 1;
                        std.debug.assert(entry.key_ptr.len > 0);
                    }
                }
            } else {
                while (iter.next()) |entry| {
                    if (entry.key_ptr.len > 0) {
                        key_buf_len += entry.key_ptr.len;
                        key_count += 1;
                        e_strings_to_allocate += 1;

                        std.debug.assert(entry.key_ptr.len > 0);
                    }
                }
            }

            if (key_buf_len > 0) {
                iter.reset();
                key_buf = try allocator.alloc(u8, key_buf_len + key_count * "process.env.".len);
                const js_ast = @import("./js_ast.zig");

                var e_strings = try allocator.alloc(js_ast.E.String, e_strings_to_allocate * 2);
                errdefer allocator.free(e_strings);
                errdefer allocator.free(key_buf);
                var key_fixed_allocator = std.heap.FixedBufferAllocator.init(key_buf);
                var key_allocator = key_fixed_allocator.allocator();

                if (behavior == .prefix) {
                    while (iter.next()) |entry| {
                        const value: string = entry.value_ptr.*;

                        if (strings.startsWith(entry.key_ptr.*, prefix)) {
                            const key_str = std.fmt.allocPrint(key_allocator, "process.env.{s}", .{entry.key_ptr.*}) catch unreachable;

                            e_strings[0] = js_ast.E.String{
                                .data = if (value.len > 0)
                                    @intToPtr([*]u8, @ptrToInt(value.ptr))[0..value.len]
                                else
                                    &[_]u8{},
                            };
                            var expr_data = js_ast.Expr.Data{ .e_string = &e_strings[0] };

                            if (e_strings[0].eqlComptime("undefined")) {
                                expr_data = .{ .e_undefined = .{} };
                            }

                            if (e_strings[0].eqlComptime("null")) {
                                expr_data = .{ .e_null = .{} };
                            }

                            if (e_strings[0].eqlComptime("false")) {
                                expr_data = .{ .e_boolean = .{ .value = false } };
                            }

                            if (e_strings[0].eqlComptime("true")) {
                                expr_data = .{ .e_boolean = .{ .value = true } };
                            }

                            if (e_strings[0].eqlComptime("0")) {
                                expr_data = .{ .e_number = .{ .value = 0.0 } };
                            }

                            if (e_strings[0].eqlComptime("1")) {
                                expr_data = .{ .e_number = .{ .value = 1.0 } };
                            }

                            _ = try to_string.getOrPutValue(
                                key_str,
                                .{
                                    .can_be_removed_if_unused = true,
                                    .call_can_be_unwrapped_if_unused = true,
                                    .value = expr_data,
                                },
                            );
                            e_strings = e_strings[1..];
                        } else {
                            const hash = std.hash.Wyhash.hash(0, entry.key_ptr.*);

                            std.debug.assert(hash != invalid_hash);

                            if (std.mem.indexOfScalar(u64, string_map_hashes, hash)) |key_i| {
                                e_strings[0] = js_ast.E.String{
                                    .data = if (value.len > 0)
                                        @intToPtr([*]u8, @ptrToInt(value.ptr))[0..value.len]
                                    else
                                        &[_]u8{},
                                };

                                var expr_data = js_ast.Expr.Data{ .e_string = &e_strings[0] };

                                if (e_strings[0].eqlComptime("undefined")) {
                                    expr_data = .{ .e_undefined = .{} };
                                }

                                if (e_strings[0].eqlComptime("null")) {
                                    expr_data = .{ .e_null = .{} };
                                }

                                if (e_strings[0].eqlComptime("false")) {
                                    expr_data = .{ .e_boolean = .{ .value = false } };
                                }

                                if (e_strings[0].eqlComptime("true")) {
                                    expr_data = .{ .e_boolean = .{ .value = true } };
                                }

                                if (e_strings[0].eqlComptime("0")) {
                                    expr_data = .{ .e_number = .{ .value = 0.0 } };
                                }

                                if (e_strings[0].eqlComptime("1")) {
                                    expr_data = .{ .e_number = .{ .value = 1.0 } };
                                }

                                _ = try to_string.getOrPutValue(
                                    framework_defaults.keys[key_i],
                                    .{
                                        .can_be_removed_if_unused = true,
                                        .call_can_be_unwrapped_if_unused = true,
                                        .value = expr_data,
                                    },
                                );
                                e_strings = e_strings[1..];
                            }
                        }
                    }
                } else {
                    while (iter.next()) |entry| {
                        const value: string = if (entry.value_ptr.*.len == 0) empty_string_value else entry.value_ptr.*;
                        const key = std.fmt.allocPrint(key_allocator, "process.env.{s}", .{entry.key_ptr.*}) catch unreachable;

                        e_strings[0] = js_ast.E.String{
                            .data = if (entry.value_ptr.*.len > 0)
                                @intToPtr([*]u8, @ptrToInt(entry.value_ptr.*.ptr))[0..value.len]
                            else
                                &[_]u8{},
                        };

                        var expr_data = js_ast.Expr.Data{ .e_string = &e_strings[0] };

                        if (e_strings[0].eqlComptime("undefined")) {
                            expr_data = .{ .e_undefined = .{} };
                        }

                        if (e_strings[0].eqlComptime("null")) {
                            expr_data = .{ .e_null = .{} };
                        }

                        if (e_strings[0].eqlComptime("false")) {
                            expr_data = .{ .e_boolean = .{ .value = false } };
                        }

                        if (e_strings[0].eqlComptime("true")) {
                            expr_data = .{ .e_boolean = .{ .value = true } };
                        }

                        if (e_strings[0].eqlComptime("0")) {
                            expr_data = .{ .e_number = .{ .value = 0.0 } };
                        }

                        if (e_strings[0].eqlComptime("1")) {
                            expr_data = .{ .e_number = .{ .value = 1.0 } };
                        }

                        _ = try to_string.getOrPutValue(
                            key,
                            .{
                                .can_be_removed_if_unused = true,
                                .call_can_be_unwrapped_if_unused = true,
                                .value = expr_data,
                            },
                        );
                        e_strings = e_strings[1..];
                    }
                }
            }
        }

        for (framework_defaults.keys) |key, i| {
            var value = framework_defaults.values[i];

            if (!to_string.contains(key) and !to_json.contains(key)) {
                // is this an escaped string?
                // if so, we start with the quotes instead of the escape
                if (value.len > 2 and
                    value[0] == '\\' and
                    (value[1] == '"' or value[1] == '\'') and
                    value[value.len - 1] == '\\' and
                    (value[value.len - 2] == '"' or
                    value[value.len - 2] == '\''))
                {
                    value = value[1 .. value.len - 1];
                }

                if (strings.eqlComptime(value, "undefined")) {
                    _ = try to_string.getOrPutValue(
                        key,
                        .{
                            .can_be_removed_if_unused = true,
                            .value = .{ .e_undefined = .{} },
                        },
                    );
                    continue;
                }

                if (strings.eqlComptime(value, "null")) {
                    _ = try to_string.getOrPutValue(
                        key,
                        .{
                            .can_be_removed_if_unused = true,
                            .call_can_be_unwrapped_if_unused = true,
                            .value = .{ .e_null = .{} },
                        },
                    );
                    continue;
                }

                if (strings.eqlComptime(value, "false")) {
                    _ = try to_string.getOrPutValue(
                        key,
                        .{
                            .can_be_removed_if_unused = true,
                            .call_can_be_unwrapped_if_unused = true,
                            .value = .{ .e_boolean = .{ .value = false } },
                        },
                    );
                    continue;
                }

                if (strings.eqlComptime(value, "true")) {
                    _ = try to_string.getOrPutValue(
                        key,
                        .{
                            .can_be_removed_if_unused = true,
                            .call_can_be_unwrapped_if_unused = true,
                            .value = .{ .e_boolean = .{ .value = true } },
                        },
                    );
                    continue;
                }

                if (strings.eqlComptime(value, "0")) {
                    _ = try to_string.getOrPutValue(
                        key,
                        .{
                            .can_be_removed_if_unused = true,
                            .call_can_be_unwrapped_if_unused = true,
                            .value = .{ .e_number = .{ .value = 0.0 } },
                        },
                    );
                    continue;
                }

                if (strings.eqlComptime(value, "1")) {
                    _ = try to_string.getOrPutValue(
                        key,
                        .{
                            .can_be_removed_if_unused = true,
                            .call_can_be_unwrapped_if_unused = true,
                            .value = .{ .e_number = .{ .value = 1.0 } },
                        },
                    );
                    continue;
                }

                _ = try to_json.getOrPutValue(key, value);
            }
        }

        return key_buf;
    }

    pub fn init(map: *Map, allocator: std.mem.Allocator) Loader {
        return Loader{
            .map = map,
            .allocator = allocator,
        };
    }

    pub fn loadProcess(this: *Loader) void {
        if (this.did_load_process) return;

        // This is a little weird because it's evidently stored line-by-line
        var source = logger.Source.initPathString("process.env", "");
        for (std.os.environ) |env| {
            source.contents = std.mem.span(env);
            Parser.parse(&source, this.allocator, this.map, true, true);
        }
        this.did_load_process = true;

        if (this.map.get("HOME")) |home_folder| {
            Analytics.username_only_for_determining_project_id_and_never_sent = home_folder;
        } else if (this.map.get("USER")) |home_folder| {
            Analytics.username_only_for_determining_project_id_and_never_sent = home_folder;
        }
    }

    // mostly for tests
    pub fn loadFromString(this: *Loader, str: string, comptime overwrite: bool) void {
        var source = logger.Source.initPathString("test", str);
        Parser.parse(&source, this.allocator, this.map, overwrite, false);
        std.mem.doNotOptimizeAway(&source);
    }

    // .env.local goes first
    // Load .env.development if development
    // Load .env.production if !development
    // .env goes last
    pub fn load(
        this: *Loader,
        fs: *Fs.FileSystem.RealFS,
        dir: *Fs.FileSystem.DirEntry,
        comptime development: bool,
    ) !void {
        const start = std.time.nanoTimestamp();
        var dir_handle: std.fs.Dir = std.fs.cwd();

        if (dir.hasComptimeQuery(".env.local")) {
            try this.loadEnvFile(fs, dir_handle, ".env.local", false);
            Analytics.Features.dotenv = true;
        }

        if (comptime development) {
            if (dir.hasComptimeQuery(".env.development")) {
                try this.loadEnvFile(fs, dir_handle, ".env.development", false);
                Analytics.Features.dotenv = true;
            }
        } else {
            if (dir.hasComptimeQuery(".env.production")) {
                try this.loadEnvFile(fs, dir_handle, ".env.production", false);
                Analytics.Features.dotenv = true;
            }
        }

        if (dir.hasComptimeQuery(".env")) {
            try this.loadEnvFile(fs, dir_handle, ".env", false);
            Analytics.Features.dotenv = true;
        }

        if (!this.quiet) this.printLoaded(start);
    }

    pub fn printLoaded(this: *Loader, start: i128) void {
        const count =
            @intCast(u8, @boolToInt(this.@".env.local" != null)) +
            @intCast(u8, @boolToInt(this.@".env.development" != null)) +
            @intCast(u8, @boolToInt(this.@".env.production" != null)) +
            @intCast(u8, @boolToInt(this.@".env" != null));

        if (count == 0) return;
        const elapsed = @intToFloat(f64, (std.time.nanoTimestamp() - start)) / std.time.ns_per_ms;

        const all = [_]string{
            ".env.local",
            ".env.development",
            ".env.production",
            ".env",
        };
        const loaded = [_]bool{
            this.@".env.local" != null,
            this.@".env.development" != null,
            this.@".env.production" != null,
            this.@".env" != null,
        };

        var loaded_i: u8 = 0;
        Output.printElapsed(elapsed);
        Output.prettyError(" <d>", .{});

        for (loaded) |yes, i| {
            if (yes) {
                loaded_i += 1;
                if (count == 1 or (loaded_i >= count and count > 1)) {
                    Output.prettyError("\"{s}\"", .{all[i]});
                } else {
                    Output.prettyError("\"{s}\", ", .{all[i]});
                }
            }
        }
        Output.prettyErrorln("<r>\n", .{});
        Output.flush();
    }

    pub fn loadEnvFile(this: *Loader, fs: *Fs.FileSystem.RealFS, dir: std.fs.Dir, comptime base: string, comptime override: bool) !void {
        if (@field(this, base) != null) {
            return;
        }

        var file = dir.openFile(base, .{ .mode = .read_only }) catch |err| {
            switch (err) {
                error.FileNotFound => {
                    // prevent retrying
                    @field(this, base) = logger.Source.initPathString(base, "");
                    return;
                },
                error.FileBusy, error.DeviceBusy, error.AccessDenied, error.IsDir => {
                    if (!this.quiet) {
                        Output.prettyErrorln("<r><red>{s}<r> error loading {s} file", .{ @errorName(err), base });
                    }

                    // prevent retrying
                    @field(this, base) = logger.Source.initPathString(base, "");
                    return;
                },
                else => {
                    return err;
                },
            }
        };
        Fs.FileSystem.setMaxFd(file.handle);

        defer {
            if (fs.needToCloseFiles()) {
                file.close();
            }
        }

        const stat = try file.stat();
        if (stat.size == 0) {
            @field(this, base) = logger.Source.initPathString(base, "");
            return;
        }

        var buf = try this.allocator.allocSentinel(u8, stat.size, 0);
        errdefer this.allocator.free(buf);
        var contents = try file.readAll(buf);
        // always sentinel
        buf.ptr[contents + 1] = 0;
        const source = logger.Source.initPathString(base, buf.ptr[0..contents :0]);

        Parser.parse(
            &source,
            this.allocator,
            this.map,
            override,
            false,
        );

        @field(this, base) = source;
    }
};

pub const Parser = struct {
    pub fn parse(
        source: *const logger.Source,
        allocator: std.mem.Allocator,
        map: *Map,
        comptime override: bool,
        comptime is_process: bool,
    ) void {
        var lexer = Lexer.init(source);
        var fbs = std.io.fixedBufferStream(&temporary_nested_value_buffer);
        var writer = fbs.writer();
        const start_count = map.map.count();

        while (lexer.next(is_process)) |variable| {
            if (variable.has_nested_value) {
                writer.context.reset();

                lexer.eatNestedValue(Map, map, @TypeOf(writer), writer, variable, Map.get_) catch unreachable;
                const new_value = fbs.buffer[0..fbs.pos];
                if (new_value.len > 0) {
                    if (comptime override) {
                        map.put(variable.key, allocator.dupe(u8, new_value) catch unreachable) catch unreachable;
                    } else {
                        var putter = map.map.getOrPut(variable.key) catch unreachable;
                        // Allow keys defined later in the same file to override keys defined earlier
                        // https://github.com/oven-sh/bun/issues/1262
                        if (!putter.found_existing or putter.index >= start_count) {
                            if (putter.found_existing and putter.value_ptr.len > 0) {
                                allocator.free(putter.value_ptr.*);
                            }

                            putter.value_ptr.* = allocator.dupe(u8, new_value) catch unreachable;
                        }
                    }
                }
            } else {
                if (comptime override) {
                    map.put(variable.key, variable.value) catch unreachable;
                } else {
                    // Allow keys defined later in the same file to override keys defined earlier
                    // https://github.com/oven-sh/bun/issues/1262
                    var putter = map.map.getOrPut(variable.key) catch unreachable;
                    if (!putter.found_existing or putter.index >= start_count) {
                        if (putter.found_existing and putter.value_ptr.len > 0) {
                            allocator.free(putter.value_ptr.*);
                        }

                        putter.value_ptr.* = allocator.dupe(u8, variable.value) catch unreachable;
                    }
                }
            }
        }
    }
};

pub const Map = struct {
    const HashTable = std.StringArrayHashMap(string);

    map: HashTable,

    pub fn createNullDelimitedEnvMap(this: *Map, arena: std.mem.Allocator) ![:null]?[*:0]u8 {
        var env_map = &this.map;

        const envp_count = env_map.count();
        const envp_buf = try arena.allocSentinel(?[*:0]u8, envp_count, null);
        {
            var it = env_map.iterator();
            var i: usize = 0;
            while (it.next()) |pair| : (i += 1) {
                const env_buf = try arena.allocSentinel(u8, pair.key_ptr.len + pair.value_ptr.len + 1, 0);
                std.mem.copy(u8, env_buf, pair.key_ptr.*);
                env_buf[pair.key_ptr.len] = '=';
                std.mem.copy(u8, env_buf[pair.key_ptr.len + 1 ..], pair.value_ptr.*);
                envp_buf[i] = env_buf.ptr;
            }
            std.debug.assert(i == envp_count);
        }
        return envp_buf;
    }

    pub fn cloneToEnvMap(this: *Map, allocator: std.mem.Allocator) !std.process.EnvMap {
        var env_map = std.process.EnvMap.init(allocator);

        const Convert = struct {
            pub fn constStrToU8(s: string) []u8 {
                return @intToPtr([*]u8, @ptrToInt(s.ptr))[0..s.len];
            }
        };

        var iter_ = this.map.iterator();
        while (iter_.next()) |entry| {
            try env_map.putMove(Convert.constStrToU8(entry.key_ptr.*), Convert.constStrToU8(entry.value_ptr.*));
        }

        return env_map;
    }

    pub inline fn init(allocator: std.mem.Allocator) Map {
        return Map{ .map = HashTable.init(allocator) };
    }

    pub inline fn iter(this: *Map) HashTable.Iterator {
        return this.map.iterator();
    }

    pub inline fn put(this: *Map, key: string, value: string) !void {
        try this.map.put(key, value);
    }

    pub fn jsonStringify(self: *const @This(), options: anytype, writer: anytype) !void {
        var iterator = self.map.iterator();

        _ = try writer.writeAll("{");
        while (iterator.next()) |entry| {
            _ = try writer.writeAll("\n    ");

            std.json.stringify(entry.key_ptr.*, options, writer) catch unreachable;

            _ = try writer.writeAll(": ");

            std.json.stringify(entry.value_ptr.*, options, writer) catch unreachable;

            if (iterator.index <= self.map.count() - 1) {
                _ = try writer.writeAll(", ");
            }
        }

        try writer.writeAll("\n}");
    }

    pub inline fn get(
        this: *const Map,
        key: string,
    ) ?string {
        return this.map.get(key);
    }

    pub fn get_(
        this: *const Map,
        key: string,
    ) ?string {
        return this.map.get(key);
    }

    pub inline fn putDefault(this: *Map, key: string, value: string) !void {
        _ = try this.map.getOrPutValue(key, value);
    }

    pub inline fn getOrPut(this: *Map, key: string, value: string) !void {
        _ = try this.map.getOrPutValue(key, value);
    }
};

pub var instance: ?*Loader = null;

const expectString = std.testing.expectEqualStrings;
const expect = std.testing.expect;
test "DotEnv Loader - basic" {
    const VALID_ENV =
        \\API_KEY=verysecure
        \\process.env.WAT=ABCDEFGHIJKLMNOPQRSTUVWXYZZ10239457123
        \\DOUBLE-QUOTED_SHOULD_PRESERVE_NEWLINES="
        \\ya
        \\"
        \\DOUBLE_QUOTES_ESCAPABLE="\"yoooo\""
        \\SINGLE_QUOTED_SHOULDNT_PRESERVE_NEWLINES='yo
        \\'
        \\
        \\SINGLE_QUOTED_DOESNT_PRESERVES_QUOTES='yo'
        \\
        \\# Line Comment
        \\UNQUOTED_SHOULDNT_PRESERVE_NEWLINES_AND_TRIMS_TRAILING_SPACE=yo # Inline Comment
        \\
        \\      LEADING_SPACE_IS_TRIMMED=yes
        \\
        \\LEADING_SPACE_IN_UNQUOTED_VALUE_IS_TRIMMED=        yes
        \\
        \\SPACE_BEFORE_EQUALS_SIGN    =yes
        \\
        \\LINES_WITHOUT_EQUAL_ARE_IGNORED
        \\
        \\NO_VALUE_IS_EMPTY_STRING=
        \\LINES_WITHOUT_EQUAL_ARE_IGNORED
        \\
        \\IGNORING_DOESNT_BREAK_OTHER_LINES='yes'
        \\
        \\NESTED_VALUE='$API_KEY'
        \\
        \\NESTED_VALUE_WITH_CURLY_BRACES='${API_KEY}'
        \\NESTED_VALUE_WITHOUT_OPENING_CURLY_BRACE='$API_KEY}'
        \\
        \\RECURSIVE_NESTED_VALUE=$NESTED_VALUE:$API_KEY
        \\
        \\RECURSIVE_NESTED_VALUE_WITH_CURLY_BRACES=${NESTED_VALUE}:${API_KEY}
        \\
        \\NESTED_VALUES_RESPECT_ESCAPING='\$API_KEY'
        \\
        \\NESTED_VALUES_WITH_CURLY_BRACES_RESPECT_ESCAPING='\${API_KEY}'
        \\
        \\EMPTY_SINGLE_QUOTED_VALUE_IS_EMPTY_STRING=''
        \\
        \\EMPTY_DOUBLE_QUOTED_VALUE_IS_EMPTY_STRING=""
        \\
        \\VALUE_WITH_MULTIPLE_VALUES_SET_IN_SAME_FILE=''
        \\
        \\VALUE_WITH_MULTIPLE_VALUES_SET_IN_SAME_FILE='good'
        \\
    ;
    const source = logger.Source.initPathString(".env", VALID_ENV);
    var map = Map.init(default_allocator);
    inline for (.{ true, false }) |override| {
        Parser.parse(
            &source,
            default_allocator,
            &map,
            override,
            false,
        );
        try expectString(map.get("NESTED_VALUES_RESPECT_ESCAPING").?, "\\$API_KEY");
        try expectString(map.get("NESTED_VALUES_WITH_CURLY_BRACES_RESPECT_ESCAPING").?, "\\${API_KEY}");

        try expectString(map.get("NESTED_VALUE").?, "verysecure");
        try expectString(map.get("NESTED_VALUE_WITH_CURLY_BRACES").?, "verysecure");
        try expectString(map.get("NESTED_VALUE_WITHOUT_OPENING_CURLY_BRACE").?, "verysecure}");
        try expectString(map.get("RECURSIVE_NESTED_VALUE").?, "verysecure:verysecure");
        try expectString(map.get("RECURSIVE_NESTED_VALUE_WITH_CURLY_BRACES").?, "verysecure:verysecure");

        try expectString(map.get("API_KEY").?, "verysecure");
        try expectString(map.get("process.env.WAT").?, "ABCDEFGHIJKLMNOPQRSTUVWXYZZ10239457123");
        try expectString(map.get("DOUBLE-QUOTED_SHOULD_PRESERVE_NEWLINES").?, "\nya\n");
        try expectString(map.get("SINGLE_QUOTED_SHOULDNT_PRESERVE_NEWLINES").?, "yo");
        try expectString(map.get("SINGLE_QUOTED_DOESNT_PRESERVES_QUOTES").?, "yo");
        try expectString(map.get("UNQUOTED_SHOULDNT_PRESERVE_NEWLINES_AND_TRIMS_TRAILING_SPACE").?, "yo");
        try expect(map.get("LINES_WITHOUT_EQUAL_ARE_IGNORED") == null);
        try expectString(map.get("LEADING_SPACE_IS_TRIMMED").?, "yes");
        try expect(map.get("NO_VALUE_IS_EMPTY_STRING").?.len == 0);
        try expectString(map.get("IGNORING_DOESNT_BREAK_OTHER_LINES").?, "yes");
        try expectString(map.get("LEADING_SPACE_IN_UNQUOTED_VALUE_IS_TRIMMED").?, "yes");
        try expectString(map.get("SPACE_BEFORE_EQUALS_SIGN").?, "yes");
        try expectString(map.get("EMPTY_SINGLE_QUOTED_VALUE_IS_EMPTY_STRING").?, "");
        try expectString(map.get("EMPTY_DOUBLE_QUOTED_VALUE_IS_EMPTY_STRING").?, "");
        try expectString(map.get("VALUE_WITH_MULTIPLE_VALUES_SET_IN_SAME_FILE").?, "good");
    }
}

test "DotEnv Loader - Nested values with curly braces" {
    const VALID_ENV =
        \\DB_USER=postgres
        \\DB_PASS=xyz
        \\DB_HOST=localhost
        \\DB_PORT=5432
        \\DB_NAME=db
        \\
        \\DB_USER2=${DB_USER}
        \\
        \\DATABASE_URL="postgresql://${DB_USER}:${DB_PASS}@${DB_HOST}:${DB_PORT}/${DB_NAME}?pool_timeout=30&connection_limit=22"
        \\
    ;
    const source = logger.Source.initPathString(".env", VALID_ENV);
    var map = Map.init(default_allocator);
    Parser.parse(
        &source,
        default_allocator,
        &map,
        true,
        false,
    );
    try expectString(map.get("DB_USER").?, "postgres");
    try expectString(map.get("DB_USER2").?, "postgres");
    try expectString(map.get("DATABASE_URL").?, "postgresql://postgres:xyz@localhost:5432/db?pool_timeout=30&connection_limit=22");
}

test "DotEnv Process" {
    var map = Map.init(default_allocator);
    var process = try std.process.getEnvMap(default_allocator);
    var loader = Loader.init(&map, default_allocator);
    loader.loadProcess();

    try expectString(loader.map.get("TMPDIR").?, std.os.getenvZ("TMPDIR").?);
    try expect(loader.map.get("TMPDIR").?.len > 0);

    try expectString(loader.map.get("USER").?, process.get("USER").?);
    try expect(loader.map.get("USER").?.len > 0);
    try expectString(loader.map.get("HOME").?, process.get("HOME").?);
    try expect(loader.map.get("HOME").?.len > 0);
}

test "DotEnv Loader - copyForDefine" {
    const UserDefine = std.StringArrayHashMap(string);
    const UserDefinesArray = @import("./defines.zig").UserDefinesArray;
    var map = Map.init(default_allocator);
    var loader = Loader.init(&map, default_allocator);
    const framework_keys = [_]string{ "process.env.BACON", "process.env.HOSTNAME" };
    const framework_values = [_]string{ "true", "\"localhost\"" };
    const framework = Api.StringMap{
        .keys = std.mem.span(&framework_keys),
        .values = std.mem.span(&framework_values),
    };

    const user_overrides: string =
        \\BACON=false
        \\HOSTNAME=example.com
        \\THIS_SHOULDNT_BE_IN_DEFINES_MAP=true
        \\
    ;

    const skip_user_overrides: string =
        \\THIS_SHOULDNT_BE_IN_DEFINES_MAP=true
        \\
    ;

    loader.loadFromString(skip_user_overrides, false);

    var user_defines = UserDefine.init(default_allocator);
    var env_defines = UserDefinesArray.init(default_allocator);
    var buf = try loader.copyForDefine(UserDefine, &user_defines, UserDefinesArray, &env_defines, framework, .disable, "", default_allocator);

    try expect(user_defines.get("process.env.THIS_SHOULDNT_BE_IN_DEFINES_MAP") == null);

    user_defines = UserDefine.init(default_allocator);
    env_defines = UserDefinesArray.init(default_allocator);

    loader.loadFromString(user_overrides, true);

    buf = try loader.copyForDefine(
        UserDefine,
        &user_defines,
        UserDefinesArray,
        &env_defines,
        framework,
        Api.DotEnvBehavior.load_all,
        "",
        default_allocator,
    );

    try expect(env_defines.get("process.env.BACON") != null);
    try expectString(env_defines.get("process.env.BACON").?.value.e_string.data, "false");
    try expectString(env_defines.get("process.env.HOSTNAME").?.value.e_string.data, "example.com");
    try expect(env_defines.get("process.env.THIS_SHOULDNT_BE_IN_DEFINES_MAP") != null);

    user_defines = UserDefine.init(default_allocator);
    env_defines = UserDefinesArray.init(default_allocator);

    buf = try loader.copyForDefine(UserDefine, &user_defines, UserDefinesArray, &env_defines, framework, .prefix, "HO", default_allocator);

    try expectString(env_defines.get("process.env.HOSTNAME").?.value.e_string.data, "example.com");
    try expect(env_defines.get("process.env.THIS_SHOULDNT_BE_IN_DEFINES_MAP") == null);
}
