const ConfigHeader = @This();

const std = @import("std");
const Io = std.Io;
const Configuration = std.Build.Configuration;
const Writer = std.Io.Writer;
const Path = std.Build.Cache.Path;
const Allocator = std.mem.Allocator;

const Step = @import("../Step.zig");
const Maker = @import("../../Maker.zig");

const header_text = "This file was generated by ConfigHeader using the Zig Build System.";
const c_generated_line = "/* " ++ header_text ++ " */";
const asm_generated_line = "; " ++ header_text;

const os_newline = if (@import("builtin").os.tag == .windows) "\r\n" else "\n";

/// Table value is whether the value is used.
const ValueMap = std.array_hash_map.String(bool);
const Value = Configuration.Step.ConfigHeader.Value;

pub fn make(
    config_header: *ConfigHeader,
    step_index: Configuration.Step.Index,
    maker: *Maker,
    progress_node: std.Progress.Node,
) Step.ExtendedMakeError!void {
    _ = config_header;
    const graph = maker.graph;
    const step = maker.stepByIndex(step_index);
    const io = graph.io;
    const arena = graph.arena; // TODO don't leak into the process arena
    const conf = &maker.scanned_config.configuration;
    const conf_step = step_index.ptr(conf);
    const conf_ch = conf_step.extended.get(conf.extra).config_header;
    const cache_root = graph.local_cache_root;

    const input_size_limit: Io.Limit = if (conf_ch.input_size_limit.value) |x| .limited64(x) else .unlimited;
    const include_guard_override: ?[]const u8 = if (conf_ch.include_guard.value) |s| s.slice(conf) else null;
    const include_path: []const u8 = conf_ch.include_path.slice(conf);
    const template_file = if (conf_ch.template_file.value) |lp|
        try maker.resolveLazyPathIndex(arena, lp, step_index)
    else
        null;
    const value_pairs = conf_ch.values.slice;

    if (conf_ch.template_file.value) |lp| try step.singleUnchangingWatchInput(maker, arena, lp.get(conf));

    var value_map: ValueMap = .empty;
    try value_map.ensureTotalCapacity(arena, value_pairs.len);
    for (value_pairs) |pair| value_map.putAssumeCapacityNoClobber(pair.key.slice(conf), false);

    var man = graph.cache.obtain();
    defer man.deinit();

    // Random bytes to make ConfigHeader unique. Refresh this with new
    // random bytes when ConfigHeader implementation is modified in a
    // non-backwards-compatible way.
    man.hash.add(@as(u32, 0xdef08d23));
    man.hash.add(@as(u32, @bitCast(conf_ch.flags)));
    man.hash.addBytes(include_path);
    man.hash.addOptionalBytes(include_guard_override);

    var aw: Writer.Allocating = .init(arena);
    defer aw.deinit();

    switch (conf_ch.flags.style) {
        .autoconf_undef => {
            const tf = template_file.?;
            const contents = tf.root_dir.handle.readFileAlloc(io, tf.sub_path, arena, input_size_limit) catch |err|
                return step.fail(maker, "unable to read autoconf input file {f}: {t}", .{ tf, err });
            renderAutoConfUndef(maker, step, contents, &aw.writer, value_pairs, &value_map, tf) catch |err| switch (err) {
                error.WriteFailed => return error.OutOfMemory,
                else => |e| return e,
            };
        },
        .autoconf_at => {
            const tf = template_file.?;
            const contents = tf.root_dir.handle.readFileAlloc(io, tf.sub_path, arena, input_size_limit) catch |err|
                return step.fail(maker, "unable to read autoconf input file {f}: {t}", .{ tf, err });
            renderAutoconfAt(maker, step, contents, &aw, value_pairs, &value_map, tf) catch |err| switch (err) {
                error.WriteFailed => return error.OutOfMemory,
                else => |e| return e,
            };
        },
        .cmake => {
            const tf = template_file.?;
            const contents = tf.root_dir.handle.readFileAlloc(io, tf.sub_path, arena, input_size_limit) catch |err|
                return step.fail(maker, "unable to read cmake input file {f}: {t}", .{ tf, err });
            renderCmake(arena, maker, step, contents, &aw.writer, value_pairs, &value_map, tf) catch |err| switch (err) {
                error.WriteFailed => return error.OutOfMemory,
                else => |e| return e,
            };
        },
        .blank => {
            renderBlank(conf, &aw.writer, value_pairs, &value_map, include_path, include_guard_override) catch |err| switch (err) {
                error.WriteFailed => return error.OutOfMemory,
                else => |e| return e,
            };
        },
        .nasm => {
            renderNasm(conf, &aw.writer, value_pairs, &value_map) catch |err| switch (err) {
                error.WriteFailed => return error.OutOfMemory,
                else => |e| return e,
            };
        },
    }

    const output = aw.written();
    man.hash.addBytes(output);

    if (try step.cacheHit(maker, &man, progress_node)) {
        const digest = man.final();
        maker.generatedPath(conf_ch.generated_dir).* = .{
            .root_dir = cache_root,
            .sub_path = try Io.Dir.path.join(arena, &.{ "o", &digest }),
        };
        return;
    }

    const digest = man.final();

    // If output_path has directory parts, deal with them.  Example:
    // output_dir is zig-cache/o/HASH
    // output_path is libavutil/avconfig.h
    // We want to open directory zig-cache/o/HASH/libavutil/
    // but keep output_dir as zig-cache/o/HASH for -I include
    const out_path: Path = .{
        .root_dir = cache_root,
        .sub_path = try Io.Dir.path.join(arena, &.{ "o", &digest, conf_ch.include_path.slice(conf) }),
    };
    const out_path_dirname = out_path.dirname().?;

    out_path_dirname.root_dir.handle.createDirPath(io, out_path_dirname.sub_path) catch |err|
        return step.fail(maker, "unable to make path {f}: {t}", .{ out_path_dirname, err });

    out_path.root_dir.handle.writeFile(io, .{ .sub_path = out_path.sub_path, .data = output }) catch |err|
        return step.fail(maker, "unable to write file {f}: {t}", .{ out_path, err });

    maker.generatedPath(conf_ch.generated_dir).* = .{
        .root_dir = cache_root,
        .sub_path = try Io.Dir.path.join(arena, &.{ "o", &digest }),
    };

    try step.writeManifest(maker, &man);
}

fn ensureAllValuesUsed(
    maker: *Maker,
    step: *Step,
    value_map: *const ValueMap,
    src_path: Path,
) Step.ExtendedMakeError!void {
    var any_errors = false;
    for (value_map.keys(), value_map.values()) |name, used| {
        if (used) continue;
        try step.addError(maker, "{f}: config header value unused: {s}", .{ src_path, name });
        any_errors = true;
    }
    if (any_errors) return error.MakeFailed;
}

fn detectNewline(contents: []const u8) []const u8 {
    const lf_index = std.mem.findScalar(u8, contents, '\n') orelse return os_newline;
    return if (lf_index > 0 and contents[lf_index - 1] == '\r') "\r\n" else "\n";
}

fn renderAutoConfUndef(
    maker: *Maker,
    step: *Step,
    contents: []const u8,
    w: *Writer,
    value_pairs: []const Value.Pair,
    value_map: *ValueMap,
    src_path: Path,
) !void {
    const conf = &maker.scanned_config.configuration;
    const newline = detectNewline(contents);

    try w.writeAll(c_generated_line);
    try w.writeAll(newline);

    var any_errors = false;
    var line_index: u32 = 0;
    var line_it = std.mem.splitScalar(u8, contents, '\n');
    while (line_it.next()) |raw_line| : (line_index += 1) {
        const last_line = line_it.index == line_it.buffer.len;
        const line = std.mem.trimEnd(u8, raw_line, "\r");

        if (!std.mem.startsWith(u8, line, "#")) {
            try w.writeAll(line);
            if (!last_line) try w.writeAll(newline);
            continue;
        }
        var it = std.mem.tokenizeAny(u8, line[1..], " \t\r");
        const undef = it.next().?;
        if (!std.mem.eql(u8, undef, "undef")) {
            try w.writeAll(line);
            if (!last_line) try w.writeAll(newline);
            continue;
        }
        const name = it.next().?;
        const index = value_map.getIndex(name) orelse {
            try step.addError(maker, "{f}:{d}: unspecified config header value: {s}", .{
                src_path, line_index + 1, name,
            });
            any_errors = true;
            continue;
        };
        value_map.values()[index] = true; // Set to used.
        try renderValueC(conf, w, newline, name, value_pairs[index].index);
    }

    try ensureAllValuesUsed(maker, step, value_map, src_path);
    if (any_errors) return error.MakeFailed;
}

fn renderAutoconfAt(
    maker: *Maker,
    step: *Step,
    contents: []const u8,
    aw: *Writer.Allocating,
    value_pairs: []const Value.Pair,
    value_map: *const ValueMap,
    src_path: Path,
) !void {
    const w = &aw.writer;
    const conf = &maker.scanned_config.configuration;
    const newline = detectNewline(contents);

    try w.writeAll(c_generated_line);
    try w.writeAll(newline);

    var any_errors = false;
    var line_index: u32 = 0;
    var line_it = std.mem.splitScalar(u8, contents, '\n');
    while (line_it.next()) |raw_line| : (line_index += 1) {
        const last_line = line_it.index == line_it.buffer.len;
        const line = std.mem.trimEnd(u8, raw_line, "\r");

        const old_len = aw.written().len;
        expandVariablesAutoconfAt(w, line, conf, value_pairs, value_map) catch |err| switch (err) {
            error.MissingValue => {
                const name = aw.written()[old_len..];
                defer aw.shrinkRetainingCapacity(old_len);
                try step.addError(maker, "{f}:{d}: error: unspecified config header value: {s}", .{
                    src_path, line_index + 1, name,
                });
                any_errors = true;
                continue;
            },
            else => {
                try step.addError(maker, "{f}:{d}: unable to substitute variable: error: {t}", .{
                    src_path, line_index + 1, err,
                });
                any_errors = true;
                continue;
            },
        };
        if (!last_line) try w.writeAll(newline);
    }

    try ensureAllValuesUsed(maker, step, value_map, src_path);
    if (any_errors) return error.MakeFailed;
}

fn renderCmake(
    arena: Allocator,
    maker: *Maker,
    step: *Step,
    contents: []const u8,
    w: *Writer,
    value_pairs: []const Value.Pair,
    value_map: *ValueMap,
    src_path: Path,
) !void {
    const conf = &maker.scanned_config.configuration;
    const newline = detectNewline(contents);

    try w.writeAll(c_generated_line);
    try w.writeAll(newline);

    var any_errors = false;
    var line_index: u32 = 0;
    var line_it = std.mem.splitScalar(u8, contents, '\n');
    while (line_it.next()) |raw_line| : (line_index += 1) {
        const last_line = line_it.index == line_it.buffer.len;
        const stripped_line = std.mem.trimEnd(u8, raw_line, "\r");

        const line = expandVariablesCmake(arena, stripped_line, conf, value_pairs, value_map) catch |err| switch (err) {
            error.InvalidCharacter => {
                try step.addError(maker, "{f}:{d}: invalid character in a variable name", .{
                    src_path, line_index + 1,
                });
                any_errors = true;
                continue;
            },
            else => {
                try step.addError(maker, "{f}:{d}: failed substituting variable: {t}", .{
                    src_path, line_index + 1, err,
                });
                any_errors = true;
                continue;
            },
        };

        const line_start = std.mem.findNone(u8, line, " \t\r") orelse {
            try w.writeAll(line);
            if (!last_line) try w.writeAll(newline);
            continue;
        };
        const whitespace_prefix = line[0..line_start];
        const trimmed_line = line[line_start..];

        if (!std.mem.startsWith(u8, trimmed_line, "#")) {
            try w.writeAll(line);
            if (!last_line) try w.writeAll(newline);
            continue;
        }

        var it = std.mem.tokenizeAny(u8, trimmed_line[1..], " \t\r");
        const cmakedefine = it.next().?;

        const booldefine = if (std.mem.eql(u8, cmakedefine, "cmakedefine01"))
            true
        else if (std.mem.eql(u8, cmakedefine, "cmakedefine"))
            false
        else {
            try w.writeAll(line);
            if (!last_line) try w.writeAll(newline);
            continue;
        };

        const name = it.next() orelse {
            try step.addError(maker, "{f}:{d}: error: missing define name", .{ src_path, line_index + 1 });
            any_errors = true;
            continue;
        };
        const orig_value: Value.Index = v: {
            const index = value_map.getIndex(name) orelse break :v if (booldefine) .int_0 else .undef;
            value_map.values()[index] = true; // Mark as used.
            break :v value_pairs[index].index;
        };
        const value = switch (orig_value.unpack(conf)) {
            .bool => |b| if (!b) .undef else orig_value,
            inline .i64, .u64 => |i| if (i == 0) .undef else orig_value,
            .string => |s| if (s.len == 0) .undef else orig_value,
            else => orig_value,
        };

        try w.writeAll(whitespace_prefix);

        if (booldefine) {
            try renderValueCBool(w, newline, name, switch (value.unpack(conf)) {
                .undef, .defined => false,
                .bool => |b| b,
                inline .u64, .i64 => |i| i != 0,
                .string => |s| s.len != 0,
                .ident => false,
            });
        } else if (value != .undef) {
            try renderValueCIdent(w, newline, name, it.rest());
        } else {
            try renderValueC(conf, w, newline, name, value);
        }
    }

    try ensureAllValuesUsed(maker, step, value_map, src_path);
    if (any_errors) return error.MakeFailed;
}

fn renderBlank(
    conf: *const Configuration,
    w: *Writer,
    value_pairs: []const Value.Pair,
    value_map: *const ValueMap,
    include_path: []const u8,
    include_guard_override: ?[]const u8,
) !void {
    try w.writeAll(c_generated_line);
    try w.writeAll(os_newline);

    const include_guard_fmt: IncludeGuardFmt = .{
        .include_path = include_path,
        .override = include_guard_override,
    };

    try w.print(
        "#ifndef {[0]f}{[1]s}#define {[0]f}{[1]s}",
        .{ include_guard_fmt, os_newline },
    );

    for (value_map.keys(), value_pairs) |name, pair| try renderValueC(conf, w, os_newline, name, pair.index);

    try w.print("#endif /* {f} */{s}", .{ include_guard_fmt, os_newline });
}

const IncludeGuardFmt = struct {
    include_path: []const u8,
    override: ?[]const u8,

    pub fn format(this: @This(), w: *Writer) Writer.Error!void {
        if (this.override) |s| return w.writeAll(s);
        for (this.include_path) |byte| switch (byte) {
            'a'...'z' => try w.writeByte(byte - 'a' + 'A'),
            'A'...'Z', '0'...'9' => continue,
            else => try w.writeByte('_'),
        };
    }
};

fn renderNasm(
    conf: *const Configuration,
    w: *Writer,
    value_pairs: []const Value.Pair,
    value_map: *const ValueMap,
) !void {
    try w.writeAll(asm_generated_line);
    try w.writeAll(os_newline);
    for (value_map.keys(), value_pairs) |name, pair| try renderValueNasm(conf, w, os_newline, name, pair.index);
}

fn renderValueC(conf: *const Configuration, w: *Writer, newline: []const u8, name: []const u8, value: Value.Index) !void {
    switch (value.unpack(conf)) {
        .undef => try w.print("/* #undef {s} */{s}", .{ name, newline }),
        .defined => try w.print("#define {s}{s}", .{ name, newline }),
        .bool => |b| return renderValueCBool(w, newline, name, b),
        inline .u64, .i64 => |i| try w.print("#define {s} {d}{s}", .{ name, i, newline }),
        .ident => |ident| return renderValueCIdent(w, newline, name, ident),
        .string => |string| try w.print("#define {s} \"{f}\"{s}", .{ name, std.zig.fmtString(string), newline }),
    }
}

fn renderValueCIdent(w: *Writer, newline: []const u8, name: []const u8, ident: []const u8) Writer.Error!void {
    try w.print("#define {s}", .{name});
    if (ident.len > 0) {
        try w.writeByte(' ');
        try w.writeAll(ident);
    }
    return w.writeAll(newline);
}

fn renderValueCBool(w: *Writer, newline: []const u8, name: []const u8, b: bool) Writer.Error!void {
    return w.print("#define {s} {c}{s}", .{ name, @as(u8, '0') + @intFromBool(b), newline });
}

fn renderValueNasm(conf: *const Configuration, w: *Writer, newline: []const u8, name: []const u8, value: Value.Index) !void {
    switch (value.unpack(conf)) {
        .undef => try w.print("; %undef {s}{s}", .{ name, newline }),
        .defined => try w.print("%define {s}{s}", .{ name, newline }),
        .bool => |b| try w.print("%define {s} {c}{s}", .{ name, @as(u8, '0') + @intFromBool(b), newline }),
        inline .u64, .i64 => |i| try w.print("%define {s} {d}{s}", .{ name, i, newline }),
        .ident => |ident| try w.print("%define {s} {s}{s}", .{ name, ident, newline }),
        .string => |string| try w.print("%define {s} \"{f}\"{s}", .{ name, std.zig.fmtString(string), newline }),
    }
}

fn expandVariablesAutoconfAt(
    w: *Writer,
    contents: []const u8,
    conf: *const Configuration,
    value_pairs: []const Value.Pair,
    value_map: *const ValueMap,
) !void {
    const valid_varname_chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_";

    var curr: usize = 0;
    var source_offset: usize = 0;
    while (curr < contents.len) : (curr += 1) {
        if (contents[curr] != '@') continue;
        if (std.mem.findScalarPos(u8, contents, curr + 1, '@')) |close_pos| {
            if (close_pos == curr + 1) {
                // closed immediately, preserve as a literal
                continue;
            }
            const valid_varname_end = std.mem.findNonePos(u8, contents, curr + 1, valid_varname_chars) orelse 0;
            if (valid_varname_end != close_pos) {
                // contains invalid characters, preserve as a literal
                continue;
            }

            const key = contents[curr + 1 .. close_pos];
            const index = value_map.getIndex(key) orelse {
                // Report the missing key to the caller.
                try w.writeAll(key);
                return error.MissingValue;
            };
            const value = value_pairs[index].index;
            value_map.values()[index] = true; // Mark as used.
            try w.writeAll(contents[source_offset..curr]);
            switch (value.unpack(conf)) {
                .undef, .defined => {},
                .bool => |b| try w.writeByte(@as(u8, '0') + @intFromBool(b)),
                inline .u64, .i64 => |i| try w.print("{d}", .{i}),
                .ident, .string => |s| try w.writeAll(s),
            }

            curr = close_pos;
            source_offset = close_pos + 1;
        }
    }

    try w.writeAll(contents[source_offset..]);
}

fn expandVariablesCmake(
    arena: Allocator,
    contents: []const u8,
    conf: *const Configuration,
    value_pairs: []const Value.Pair,
    value_map: *const ValueMap,
) ![]const u8 {
    var result: std.ArrayList(u8) = .empty;

    const valid_varname_chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789/_.+-";
    const open_var = "${";

    var curr: usize = 0;
    var source_offset: usize = 0;
    const Position = struct {
        source: usize,
        target: usize,
    };
    var var_stack: std.ArrayList(Position) = .empty;
    loop: while (curr < contents.len) : (curr += 1) {
        switch (contents[curr]) {
            '@' => blk: {
                if (std.mem.findScalarPos(u8, contents, curr + 1, '@')) |close_pos| {
                    if (close_pos == curr + 1) {
                        // closed immediately, preserve as a literal
                        break :blk;
                    }
                    const valid_varname_end = std.mem.findNonePos(u8, contents, curr + 1, valid_varname_chars) orelse 0;
                    if (valid_varname_end != close_pos) {
                        // contains invalid characters, preserve as a literal
                        break :blk;
                    }

                    const key = contents[curr + 1 .. close_pos];
                    const index = value_map.getIndex(key) orelse return error.MissingValue;
                    value_map.values()[index] = true; // Mark as used.
                    const value = value_pairs[index].index;
                    const missing = contents[source_offset..curr];
                    try result.appendSlice(arena, missing);
                    switch (value.unpack(conf)) {
                        .undef, .defined => {},
                        .bool => |b| try result.append(arena, if (b) '1' else '0'),
                        inline .i64, .u64 => |i| try result.print(arena, "{d}", .{i}),
                        .ident, .string => |s| try result.appendSlice(arena, s),
                    }

                    curr = close_pos;
                    source_offset = close_pos + 1;

                    continue :loop;
                }
            },
            '$' => blk: {
                const next = curr + 1;
                if (next == contents.len or contents[next] != '{') {
                    // no open bracket detected, preserve as a literal
                    break :blk;
                }
                const missing = contents[source_offset..curr];
                try result.appendSlice(arena, missing);
                try result.appendSlice(arena, open_var);

                source_offset = curr + open_var.len;
                curr = next;
                try var_stack.append(arena, .{
                    .source = curr,
                    .target = result.items.len - open_var.len,
                });

                continue :loop;
            },
            '}' => blk: {
                if (var_stack.items.len == 0) {
                    // no open bracket, preserve as a literal
                    break :blk;
                }
                const open_pos = var_stack.pop().?;
                if (source_offset == open_pos.source) {
                    source_offset += open_var.len;
                }
                const missing = contents[source_offset..curr];
                try result.appendSlice(arena, missing);

                const key_start = open_pos.target + open_var.len;
                const key = result.items[key_start..];
                if (key.len == 0) {
                    return error.MissingKey;
                }
                const index = value_map.getIndex(key) orelse return error.MissingValue;
                value_map.values()[index] = true; // Mark as used.
                const value = value_pairs[index].index;
                result.shrinkRetainingCapacity(result.items.len - key.len - open_var.len);
                switch (value.unpack(conf)) {
                    .undef, .defined => {},
                    .bool => |b| try result.append(arena, if (b) '1' else '0'),
                    inline .i64, .u64 => |i| try result.print(arena, "{d}", .{i}),
                    .ident, .string => |s| try result.appendSlice(arena, s),
                }

                source_offset = curr + 1;

                continue :loop;
            },
            '\\' => {
                // backslash is not considered a special character
                continue :loop;
            },
            else => {},
        }

        if (var_stack.items.len > 0 and std.mem.findScalar(u8, valid_varname_chars, contents[curr]) == null) {
            return error.InvalidCharacter;
        }
    }

    if (source_offset != contents.len) {
        const missing = contents[source_offset..];
        try result.appendSlice(arena, missing);
    }

    try result.shrinkToLen(arena);

    return result.toOwnedSliceAssert();
}
