const std = @import("std");
const gpa = std.heap.wasm_allocator;
const abi = std.Build.abi.time_report;
const fmtEscapeHtml = @import("root").fmtEscapeHtml;
const step_list = &@import("root").step_list;

const js = struct {
    extern "time_report" fn updateGeneric(
        /// The index of the step.
        step_idx: u32,
        // The HTML which will be used to populate the template slots.
        inner_html_ptr: [*]const u8,
        inner_html_len: usize,
    ) void;
    extern "time_report" fn updateCompile(
        /// The index of the step.
        step_idx: u32,
        // The HTML which will be used to populate the template slots.
        inner_html_ptr: [*]const u8,
        inner_html_len: usize,
        // The HTML which will populate the <tbody> of the file table.
        file_table_html_ptr: [*]const u8,
        file_table_html_len: usize,
        // The HTML which will populate the <tbody> of the decl table.
        decl_table_html_ptr: [*]const u8,
        decl_table_html_len: usize,
        /// Whether the LLVM backend was used. If not, LLVM-specific statistics are hidden.
        use_llvm: bool,
    ) void;
    extern "time_report" fn updateRunTest(
        /// The index of the step.
        step_idx: u32,
        // The HTML which will populate the <tbody> of the test table.
        table_html_ptr: [*]const u8,
        table_html_len: usize,
    ) void;
};

pub fn genericResultMessage(msg_bytes: []u8) error{OutOfMemory}!void {
    if (msg_bytes.len != @sizeOf(abi.GenericResult)) @panic("malformed GenericResult message");
    const msg: *const abi.GenericResult = @ptrCast(msg_bytes);
    if (msg.step_idx >= step_list.*.len) @panic("malformed GenericResult message");
    const inner_html = try std.fmt.allocPrint(gpa,
        \\<code slot="step-name">{[step_name]f}</code>
        \\<span slot="stat-total-time">{[stat_total_time]f}</span>
    , .{
        .step_name = fmtEscapeHtml(step_list.*[msg.step_idx].name),
        .stat_total_time = std.Io.Duration{ .nanoseconds = msg.ns_total },
    });
    defer gpa.free(inner_html);
    js.updateGeneric(msg.step_idx, inner_html.ptr, inner_html.len);
}

pub fn compileResultMessage(msg_bytes: []u8) error{ OutOfMemory, WriteFailed }!void {
    const max_table_rows = 500;

    if (msg_bytes.len < @sizeOf(abi.CompileResult)) @panic("malformed CompileResult message");
    const hdr: *const abi.CompileResult = @ptrCast(msg_bytes[0..@sizeOf(abi.CompileResult)]);
    if (hdr.step_idx >= step_list.*.len) @panic("malformed CompileResult message");
    var trailing = msg_bytes[@sizeOf(abi.CompileResult)..];

    const llvm_pass_timings = trailing[0..hdr.llvm_pass_timings_len];
    trailing = trailing[hdr.llvm_pass_timings_len..];

    const FileTimeReport = struct {
        name: []const u8,
        ns_sema: u64,
        ns_codegen: u64,
        ns_link: u64,
    };
    const DeclTimeReport = struct {
        file_name: []const u8,
        name: []const u8,
        sema_count: u32,
        ns_sema: u64,
        ns_codegen: u64,
        ns_link: u64,
    };

    const slowest_files = try gpa.alloc(FileTimeReport, hdr.files_len);
    defer gpa.free(slowest_files);

    const slowest_decls = try gpa.alloc(DeclTimeReport, hdr.decls_len);
    defer gpa.free(slowest_decls);

    for (slowest_files) |*file_out| {
        const i = std.mem.indexOfScalar(u8, trailing, 0) orelse @panic("malformed CompileResult message");
        file_out.* = .{
            .name = trailing[0..i],
            .ns_sema = 0,
            .ns_codegen = 0,
            .ns_link = 0,
        };
        trailing = trailing[i + 1 ..];
    }

    for (slowest_decls) |*decl_out| {
        const i = std.mem.indexOfScalar(u8, trailing, 0) orelse @panic("malformed CompileResult message");
        const file_idx = std.mem.readInt(u32, trailing[i..][1..5], .little);
        const sema_count = std.mem.readInt(u32, trailing[i..][5..9], .little);
        const sema_ns = std.mem.readInt(u64, trailing[i..][9..17], .little);
        const codegen_ns = std.mem.readInt(u64, trailing[i..][17..25], .little);
        const link_ns = std.mem.readInt(u64, trailing[i..][25..33], .little);
        const file = &slowest_files[file_idx];
        decl_out.* = .{
            .file_name = file.name,
            .name = trailing[0..i],
            .sema_count = sema_count,
            .ns_sema = sema_ns,
            .ns_codegen = codegen_ns,
            .ns_link = link_ns,
        };
        trailing = trailing[i + 33 ..];
        file.ns_sema += sema_ns;
        file.ns_codegen += codegen_ns;
        file.ns_link += link_ns;
    }

    const S = struct {
        fn fileLessThan(_: void, lhs: FileTimeReport, rhs: FileTimeReport) bool {
            const lhs_ns = lhs.ns_sema + lhs.ns_codegen + lhs.ns_link;
            const rhs_ns = rhs.ns_sema + rhs.ns_codegen + rhs.ns_link;
            return lhs_ns > rhs_ns; // flipped to sort in reverse order
        }
        fn declLessThan(_: void, lhs: DeclTimeReport, rhs: DeclTimeReport) bool {
            //if (true) return lhs.sema_count > rhs.sema_count;
            const lhs_ns = lhs.ns_sema + lhs.ns_codegen + lhs.ns_link;
            const rhs_ns = rhs.ns_sema + rhs.ns_codegen + rhs.ns_link;
            return lhs_ns > rhs_ns; // flipped to sort in reverse order
        }
    };
    std.mem.sort(FileTimeReport, slowest_files, {}, S.fileLessThan);
    std.mem.sort(DeclTimeReport, slowest_decls, {}, S.declLessThan);

    const stats = hdr.stats;
    const inner_html = try std.fmt.allocPrint(gpa,
        \\<code slot="step-name">{[step_name]f}</code>
        \\<span slot="stat-reachable-files">{[stat_reachable_files]d}</span>
        \\<span slot="stat-imported-files">{[stat_imported_files]d}</span>
        \\<span slot="stat-generic-instances">{[stat_generic_instances]d}</span>
        \\<span slot="stat-inline-calls">{[stat_inline_calls]d}</span>
        \\<span slot="stat-compilation-time">{[stat_compilation_time]f}</span>
        \\<span slot="cpu-time-parse">{[cpu_time_parse]f}</span>
        \\<span slot="cpu-time-astgen">{[cpu_time_astgen]f}</span>
        \\<span slot="cpu-time-sema">{[cpu_time_sema]f}</span>
        \\<span slot="cpu-time-codegen">{[cpu_time_codegen]f}</span>
        \\<span slot="cpu-time-link">{[cpu_time_link]f}</span>
        \\<span slot="real-time-files">{[real_time_files]f}</span>
        \\<span slot="real-time-decls">{[real_time_decls]f}</span>
        \\<span slot="real-time-llvm-emit">{[real_time_llvm_emit]f}</span>
        \\<span slot="real-time-link-flush">{[real_time_link_flush]f}</span>
        \\<pre slot="llvm-pass-timings"><code>{[llvm_pass_timings]f}</code></pre>
        \\
    , .{
        .step_name = fmtEscapeHtml(step_list.*[hdr.step_idx].name),
        .stat_reachable_files = stats.n_reachable_files,
        .stat_imported_files = stats.n_imported_files,
        .stat_generic_instances = stats.n_generic_instances,
        .stat_inline_calls = stats.n_inline_calls,
        .stat_compilation_time = std.Io.Duration{ .nanoseconds = hdr.ns_total },

        .cpu_time_parse = std.Io.Duration{ .nanoseconds = stats.cpu_ns_parse },
        .cpu_time_astgen = std.Io.Duration{ .nanoseconds = stats.cpu_ns_astgen },
        .cpu_time_sema = std.Io.Duration{ .nanoseconds = stats.cpu_ns_sema },
        .cpu_time_codegen = std.Io.Duration{ .nanoseconds = stats.cpu_ns_codegen },
        .cpu_time_link = std.Io.Duration{ .nanoseconds = stats.cpu_ns_link },
        .real_time_files = std.Io.Duration{ .nanoseconds = stats.real_ns_files },
        .real_time_decls = std.Io.Duration{ .nanoseconds = stats.real_ns_decls },
        .real_time_llvm_emit = std.Io.Duration{ .nanoseconds = stats.real_ns_llvm_emit },
        .real_time_link_flush = std.Io.Duration{ .nanoseconds = stats.real_ns_link_flush },

        .llvm_pass_timings = fmtEscapeHtml(llvm_pass_timings),
    });
    defer gpa.free(inner_html);

    var file_table_html: std.Io.Writer.Allocating = .init(gpa);
    defer file_table_html.deinit();

    for (slowest_files[0..@min(max_table_rows, slowest_files.len)]) |file| {
        try file_table_html.writer.print(
            \\<tr>
            \\  <th scope="row"><code>{f}</code></th>
            \\  <td>{f}</td>
            \\  <td>{f}</td>
            \\  <td>{f}</td>
            \\  <td>{f}</td>
            \\</tr>
            \\
        , .{
            fmtEscapeHtml(file.name),
            std.Io.Duration{ .nanoseconds = file.ns_sema },
            std.Io.Duration{ .nanoseconds = file.ns_codegen },
            std.Io.Duration{ .nanoseconds = file.ns_link },
            std.Io.Duration{ .nanoseconds = file.ns_sema + file.ns_codegen + file.ns_link },
        });
    }
    if (slowest_files.len > max_table_rows) {
        try file_table_html.writer.print(
            \\<tr><td colspan="4">{d} more rows omitted</td></tr>
            \\
        , .{slowest_files.len - max_table_rows});
    }

    var decl_table_html: std.Io.Writer.Allocating = .init(gpa);
    defer decl_table_html.deinit();

    for (slowest_decls[0..@min(max_table_rows, slowest_decls.len)]) |decl| {
        try decl_table_html.writer.print(
            \\<tr>
            \\  <th scope="row"><code>{f}</code></th>
            \\  <th scope="row"><code>{f}</code></th>
            \\  <td>{d}</td>
            \\  <td>{f}</td>
            \\  <td>{f}</td>
            \\  <td>{f}</td>
            \\  <td>{f}</td>
            \\</tr>
            \\
        , .{
            fmtEscapeHtml(decl.file_name),
            fmtEscapeHtml(decl.name),
            decl.sema_count,
            std.Io.Duration{ .nanoseconds = decl.ns_sema },
            std.Io.Duration{ .nanoseconds = decl.ns_codegen },
            std.Io.Duration{ .nanoseconds = decl.ns_link },
            std.Io.Duration{ .nanoseconds = decl.ns_sema + decl.ns_codegen + decl.ns_link },
        });
    }
    if (slowest_decls.len > max_table_rows) {
        try decl_table_html.writer.print(
            \\<tr><td colspan="6">{d} more rows omitted</td></tr>
            \\
        , .{slowest_decls.len - max_table_rows});
    }

    js.updateCompile(
        hdr.step_idx,
        inner_html.ptr,
        inner_html.len,
        file_table_html.written().ptr,
        file_table_html.written().len,
        decl_table_html.written().ptr,
        decl_table_html.written().len,
        hdr.flags.use_llvm,
    );
}

pub fn runTestResultMessage(msg_bytes: []u8) error{OutOfMemory}!void {
    if (msg_bytes.len < @sizeOf(abi.RunTestResult)) @panic("malformed RunTestResult message");
    const hdr: *const abi.RunTestResult = @ptrCast(msg_bytes[0..@sizeOf(abi.RunTestResult)]);
    if (hdr.step_idx >= step_list.*.len) @panic("malformed RunTestResult message");
    const trailing = msg_bytes[@sizeOf(abi.RunTestResult)..];

    const durations: []align(1) const u64 = @ptrCast(trailing[0 .. hdr.tests_len * 8]);
    var offset: usize = hdr.tests_len * 8;

    var table_html: std.ArrayList(u8) = .empty;
    defer table_html.deinit(gpa);

    for (durations) |test_ns| {
        const test_name_len = std.mem.indexOfScalar(u8, trailing[offset..], 0) orelse @panic("malformed RunTestResult message");
        const test_name = trailing[offset..][0..test_name_len];
        offset += test_name_len + 1;
        try table_html.print(gpa, "<tr><th scope=\"row\"><code>{f}</code></th>", .{fmtEscapeHtml(test_name)});
        if (test_ns == std.math.maxInt(u64)) {
            try table_html.appendSlice(gpa, "<td class=\"empty-cell\"></td>"); // didn't run
        } else {
            try table_html.print(gpa, "<td>{f}</td>", .{std.Io.Duration{ .nanoseconds = test_ns }});
        }
        try table_html.appendSlice(gpa, "</tr>\n");
    }

    if (offset != trailing.len) @panic("malformed RunTestResult message");

    js.updateRunTest(
        hdr.step_idx,
        table_html.items.ptr,
        table_html.items.len,
    );
}
