const std = @import("../std.zig");
const build = std.build;
const Step = build.Step;
const Builder = build.Builder;
const LibExeObjStep = build.LibExeObjStep;
const CheckFileStep = build.CheckFileStep;
const fs = std.fs;
const mem = std.mem;
const CrossTarget = std.zig.CrossTarget;

const TranslateCStep = @This();

pub const base_id = .translate_c;

step: Step,
builder: *Builder,
source: build.FileSource,
include_dirs: std.ArrayList([]const u8),
c_macros: std.ArrayList([]const u8),
output_dir: ?[]const u8,
out_basename: []const u8,
target: CrossTarget = CrossTarget{},
output_file: build.GeneratedFile,
use_stage1: ?bool = null,

pub fn create(builder: *Builder, source: build.FileSource) *TranslateCStep {
    const self = builder.allocator.create(TranslateCStep) catch unreachable;
    self.* = TranslateCStep{
        .step = Step.init(.translate_c, "translate-c", builder.allocator, make),
        .builder = builder,
        .source = source,
        .include_dirs = std.ArrayList([]const u8).init(builder.allocator),
        .c_macros = std.ArrayList([]const u8).init(builder.allocator),
        .output_dir = null,
        .out_basename = undefined,
        .output_file = build.GeneratedFile{ .step = &self.step },
    };
    source.addStepDependencies(&self.step);
    return self;
}

pub fn setTarget(self: *TranslateCStep, target: CrossTarget) void {
    self.target = target;
}

/// Creates a step to build an executable from the translated source.
pub fn addExecutable(self: *TranslateCStep) *LibExeObjStep {
    return self.builder.addExecutableSource("translated_c", build.FileSource{ .generated = &self.output_file });
}

pub fn addIncludeDir(self: *TranslateCStep, include_dir: []const u8) void {
    self.include_dirs.append(self.builder.dupePath(include_dir)) catch unreachable;
}

pub fn addCheckFile(self: *TranslateCStep, expected_matches: []const []const u8) *CheckFileStep {
    return CheckFileStep.create(self.builder, .{ .generated = &self.output_file }, self.builder.dupeStrings(expected_matches));
}

/// If the value is omitted, it is set to 1.
/// `name` and `value` need not live longer than the function call.
pub fn defineCMacro(self: *TranslateCStep, name: []const u8, value: ?[]const u8) void {
    const macro = build.constructCMacro(self.builder.allocator, name, value);
    self.c_macros.append(macro) catch unreachable;
}

/// name_and_value looks like [name]=[value]. If the value is omitted, it is set to 1.
pub fn defineCMacroRaw(self: *TranslateCStep, name_and_value: []const u8) void {
    self.c_macros.append(self.builder.dupe(name_and_value)) catch unreachable;
}

fn make(step: *Step) !void {
    const self = @fieldParentPtr(TranslateCStep, "step", step);

    var argv_list = std.ArrayList([]const u8).init(self.builder.allocator);
    try argv_list.append(self.builder.zig_exe);
    try argv_list.append("translate-c");
    try argv_list.append("-lc");

    try argv_list.append("--enable-cache");

    if (!self.target.isNative()) {
        try argv_list.append("-target");
        try argv_list.append(try self.target.zigTriple(self.builder.allocator));
    }

    for (self.include_dirs.items) |include_dir| {
        try argv_list.append("-I");
        try argv_list.append(include_dir);
    }

    for (self.c_macros.items) |c_macro| {
        try argv_list.append("-D");
        try argv_list.append(c_macro);
    }
    if (self.use_stage1) |stage1| {
        if (stage1) {
            try argv_list.append("-fstage1");
        } else {
            try argv_list.append("-fno-stage1");
        }
    } else if (self.builder.use_stage1) |stage1| {
        if (stage1) {
            try argv_list.append("-fstage1");
        } else {
            try argv_list.append("-fno-stage1");
        }
    }

    try argv_list.append(self.source.getPath(self.builder));

    const output_path_nl = try self.builder.execFromStep(argv_list.items, &self.step);
    const output_path = mem.trimRight(u8, output_path_nl, "\r\n");

    self.out_basename = fs.path.basename(output_path);
    if (self.output_dir) |output_dir| {
        const full_dest = try fs.path.join(self.builder.allocator, &[_][]const u8{ output_dir, self.out_basename });
        try self.builder.updateFile(output_path, full_dest);
    } else {
        self.output_dir = fs.path.dirname(output_path).?;
    }

    self.output_file.path = fs.path.join(
        self.builder.allocator,
        &[_][]const u8{ self.output_dir.?, self.out_basename },
    ) catch unreachable;
}