// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program.  If not, see <https://www.gnu.org/licenses/>.

const std = @import("std");
const lp = @import("lightpanda");
const js = @import("../js/js.zig");

const Frame = @import("../Frame.zig");
const logger = lp.log;

const Console = @This();

_timers: std.StringHashMapUnmanaged(u64) = .{},
_counts: std.StringHashMapUnmanaged(u64) = .{},

pub const init: Console = .{};

pub fn trace(_: *const Console, values: []js.Value, exec: *js.Execution) !void {
    logger.debug(.js, "console.trace", .{
        .stack = exec.context.local.?.stackTrace() catch "???",
        .args = ValueWriter{ .values = values },
    });
}

pub fn debug(_: *const Console, values: []js.Value, exec: *js.Execution) void {
    logger.debug(.js, "console.debug", .{ValueWriter{ .values = values }});
    appendMessage(exec, .debug, values);
}

pub fn info(_: *const Console, values: []js.Value, exec: *js.Execution) void {
    logger.info(.js, "console.info", .{ValueWriter{ .values = values }});
    appendMessage(exec, .info, values);
}

pub fn log(_: *const Console, values: []js.Value, exec: *js.Execution) void {
    logger.info(.js, "console.log", .{ValueWriter{ .values = values }});
    appendMessage(exec, .log, values);
}

pub fn warn(_: *const Console, values: []js.Value, exec: *js.Execution) void {
    logger.warn(.js, "console.warn", .{ValueWriter{ .values = values }});
    appendMessage(exec, .warn, values);
}

pub fn clear(_: *const Console) void {}

pub fn assert(_: *const Console, assertion: js.Value, values: []js.Value, exec: *js.Execution) void {
    if (assertion.toBool()) {
        return;
    }
    logger.warn(.js, "console.assert", .{ValueWriter{ .values = values }});
    appendMessage(exec, .warn, values);
}

pub fn @"error"(_: *const Console, values: []js.Value, exec: *js.Execution) void {
    logger.warn(.js, "console.error", .{ValueWriter{ .values = values, .stack = exec.context.local.?.stackTrace() catch |err| @errorName(err) orelse "???" }});
    appendMessage(exec, .@"error", values);
}

pub fn table(_: *const Console, data: js.Value, columns: ?js.Value) void {
    logger.info(.js, "console.table", .{ .data = data, .columns = columns });
}

pub fn count(self: *Console, label_: ?[]const u8, exec: *js.Execution) !void {
    const label = label_ orelse "default";
    const gop = try self._counts.getOrPut(exec.arena, label);

    var current: u64 = 0;
    if (gop.found_existing) {
        current = gop.value_ptr.*;
    } else {
        gop.key_ptr.* = try exec.arena.dupe(u8, label);
    }

    const c = current + 1;
    gop.value_ptr.* = c;

    logger.info(.js, "console.count", .{ .label = label, .count = c });
}

pub fn countReset(self: *Console, label_: ?[]const u8) !void {
    const label = label_ orelse "default";
    const kv = self._counts.fetchRemove(label) orelse {
        logger.info(.js, "console.countReset", .{ .label = label, .err = "invalid label" });
        return;
    };
    logger.info(.js, "console.countReset", .{ .label = label, .count = kv.value });
}

pub fn time(self: *Console, label_: ?[]const u8, exec: *js.Execution) !void {
    const label = label_ orelse "default";
    const gop = try self._timers.getOrPut(exec.arena, label);

    if (gop.found_existing) {
        logger.info(.js, "console.time", .{ .label = label, .err = "duplicate timer" });
        return;
    }
    gop.key_ptr.* = try exec.arena.dupe(u8, label);
    gop.value_ptr.* = timestamp();
}

pub fn timeLog(self: *Console, label_: ?[]const u8) void {
    const elapsed = timestamp();
    const label = label_ orelse "default";
    const start = self._timers.get(label) orelse {
        logger.info(.js, "console.timeLog", .{ .label = label, .err = "invalid timer" });
        return;
    };
    logger.info(.js, "console.timeLog", .{ .label = label, .elapsed = elapsed - start });
}

pub fn timeEnd(self: *Console, label_: ?[]const u8) void {
    const elapsed = timestamp();
    const label = label_ orelse "default";
    const kv = self._timers.fetchRemove(label) orelse {
        logger.info(.js, "console.timeEnd", .{ .label = label, .err = "invalid timer" });
        return;
    };

    logger.info(.js, "console.timeEnd", .{ .label = label, .elapsed = elapsed - kv.value });
}

pub fn group(_: *const Console, values: []js.Value) void {
    logger.info(.js, "console.group", .{ValueWriter{ .values = values }});
}

pub fn groupCollapsed(_: *const Console, values: []js.Value) void {
    logger.info(.js, "console.groupCollapsed", .{ValueWriter{ .values = values }});
}

pub fn groupEnd(_: *const Console) void {}

fn timestamp() u64 {
    return @import("../../datetime.zig").timestamp(.monotonic);
}

// Forwards frame-context console output to the Frame's message buffer (read by
// the `consoleLogs` tool / CDP Runtime.consoleAPICalled). Worker contexts are
// dropped — no buffer is attached there.
fn appendMessage(exec: *js.Execution, level: Frame.ConsoleLevel, values: []js.Value) void {
    switch (exec.context.global) {
        .frame => |f| f.appendConsoleMessage(level, values),
        .worker => {},
    }
}

const ValueWriter = struct {
    values: []js.Value,
    stack: ?[]const u8 = null,

    pub fn format(self: ValueWriter, writer: *std.io.Writer) !void {
        for (self.values, 1..) |value, i| {
            try writer.print("\n  arg({d}): {f}", .{ i, value });
        }
        if (self.stack) |s| {
            try writer.print("\n stack: {s}", .{s});
        }
    }

    pub fn logFmt(self: ValueWriter, _: []const u8, writer: anytype) !void {
        var buf: [32]u8 = undefined;
        for (self.values, 0..) |value, i| {
            const name = try std.fmt.bufPrint(&buf, "param.{d}", .{i});
            try writer.write(name, value);
        }
    }

    pub fn jsonStringify(self: ValueWriter, writer: *std.json.Stringify) !void {
        try writer.beginArray();
        for (self.values) |value| {
            try writer.write(value);
        }
        return writer.endArray();
    }
};

pub const JsApi = struct {
    pub const bridge = js.Bridge(Console);

    pub const Meta = struct {
        pub const name = "Console";

        // Per the console spec, members are own properties of the namespace
        // object, so Object.entries(console) returns them.
        pub const own_properties = true;

        pub const prototype_chain = bridge.prototypeChain();
        pub var class_id: bridge.ClassId = undefined;
    };

    pub const trace = bridge.function(Console.trace, .{});
    pub const debug = bridge.function(Console.debug, .{});
    pub const info = bridge.function(Console.info, .{});
    pub const log = bridge.function(Console.log, .{});
    pub const warn = bridge.function(Console.warn, .{});
    pub const clear = bridge.function(Console.clear, .{ .noop = true });
    pub const assert = bridge.function(Console.assert, .{});
    pub const @"error" = bridge.function(Console.@"error", .{});
    pub const exception = bridge.function(Console.@"error", .{});
    pub const table = bridge.function(Console.table, .{});
    pub const count = bridge.function(Console.count, .{});
    pub const countReset = bridge.function(Console.countReset, .{});
    pub const time = bridge.function(Console.time, .{});
    pub const timeLog = bridge.function(Console.timeLog, .{});
    pub const timeEnd = bridge.function(Console.timeEnd, .{});
    pub const group = bridge.function(Console.group, .{});
    pub const groupCollapsed = bridge.function(Console.groupCollapsed, .{});
    pub const groupEnd = bridge.function(Console.groupEnd, .{});
};

const testing = @import("../../testing.zig");
test "WebApi: Console" {
    try testing.htmlRunner("console", .{});
}
