const std = @import("std");
const builtin = @import("builtin");
const lib = @import("../lib.zig");
const CAllocator = lib.alloc.Allocator;
const terminal_sys = @import("../sys.zig");
const Result = @import("result.zig").Result;

/// C: GhosttySysImage
pub const Image = extern struct {
    width: u32,
    height: u32,
    data: ?[*]u8,
    data_len: usize,
};

/// C: GhosttySysDecodePngFn
pub const DecodePngFn = *const fn (
    ?*anyopaque,
    *const CAllocator,
    [*]const u8,
    usize,
    *Image,
) callconv(lib.calling_conv) bool;

/// C: GhosttySysLogLevel
pub const LogLevel = enum(c_int) {
    @"error" = 0,
    warning = 1,
    info = 2,
    debug = 3,

    pub fn fromStd(level: std.log.Level) LogLevel {
        return switch (level) {
            .err => .@"error",
            .warn => .warning,
            .info => .info,
            .debug => .debug,
        };
    }
};

/// C: GhosttySysLogFn
pub const LogFn = *const fn (
    ?*anyopaque,
    LogLevel,
    [*]const u8,
    usize,
    [*]const u8,
    usize,
) callconv(lib.calling_conv) void;

/// C: GhosttySysOption
pub const Option = enum(c_int) {
    userdata = 0,
    decode_png = 1,
    log = 2,

    pub fn InType(comptime self: Option) type {
        return switch (self) {
            .userdata => ?*const anyopaque,
            .decode_png => ?DecodePngFn,
            .log => ?LogFn,
        };
    }
};

/// Global state for the sys interface so we can call through to the C
/// callbacks from Zig.
const Global = struct {
    userdata: ?*anyopaque = null,
    decode_png: ?DecodePngFn = null,
    log: ?LogFn = null,
};

/// Global state for the C sys interface.
var global: Global = .{};

/// Zig-compatible wrapper that calls through to the stored C callback.
/// The C callback allocates the pixel data through the provided allocator,
/// so we can take ownership directly.
fn decodePngWrapper(
    alloc: std.mem.Allocator,
    data: []const u8,
) terminal_sys.DecodeError!terminal_sys.Image {
    const func = global.decode_png orelse return error.InvalidData;

    const c_alloc = CAllocator.fromZig(&alloc);
    var out: Image = undefined;
    if (!func(global.userdata, &c_alloc, data.ptr, data.len, &out)) return error.InvalidData;

    const result_data = out.data orelse return error.InvalidData;

    return .{
        .width = out.width,
        .height = out.height,
        .data = result_data[0..out.data_len],
    };
}

pub fn set(
    option: Option,
    value: ?*const anyopaque,
) callconv(lib.calling_conv) Result {
    if (comptime std.debug.runtime_safety) {
        _ = std.meta.intToEnum(Option, @intFromEnum(option)) catch {
            return .invalid_value;
        };
    }

    return switch (option) {
        inline else => |comptime_option| setTyped(
            comptime_option,
            @ptrCast(@alignCast(value)),
        ),
    };
}

fn setTyped(
    comptime option: Option,
    value: option.InType(),
) Result {
    switch (option) {
        .userdata => global.userdata = @constCast(value),
        .decode_png => {
            global.decode_png = value;
            terminal_sys.decode_png = if (value != null) &decodePngWrapper else null;
        },
        .log => global.log = value,
    }
    return .success;
}

/// Dispatch a log message to the installed C callback, if any.
fn emitLog(level: LogLevel, scope: []const u8, message: []const u8) void {
    const func = global.log orelse return;
    func(
        global.userdata,
        level,
        scope.ptr,
        scope.len,
        message.ptr,
        message.len,
    );
}

/// Emits logs in chunks. Almost all logs will be less than the chunk size
/// but this allows emitting larger logs without heap allocation.
const LogEmitter = struct {
    c_level: LogLevel,
    scope_text: []const u8,
    buf: [2048]u8 = undefined,
    pos: usize = 0,

    fn write(self: *@This(), bytes: []const u8) error{}!usize {
        var remaining = bytes;
        while (remaining.len > 0) {
            const space = self.buf.len - self.pos;
            if (space == 0) {
                self.flush();
                continue;
            }

            const n = @min(remaining.len, space);
            @memcpy(self.buf[self.pos..][0..n], remaining[0..n]);
            self.pos += n;
            remaining = remaining[n..];
        }

        return bytes.len;
    }

    fn flush(self: *@This()) void {
        if (self.pos == 0) return;
        emitLog(
            self.c_level,
            self.scope_text,
            self.buf[0..self.pos],
        );
        self.pos = 0;
    }
};

/// Custom std.log sink for C ABI builds.
///
/// When a log callback is installed via ghostty_sys_set(), messages are
/// dispatched through it. When no callback is installed, messages are
/// silently discarded. Large messages that exceed the stack buffer are
/// delivered across multiple callback invocations.
pub fn logFn(
    comptime level: std.log.Level,
    comptime scope: @TypeOf(.EnumLiteral),
    comptime format: []const u8,
    args: anytype,
) void {
    if (global.log == null) return;

    const scope_text: []const u8 = if (scope == .default) "" else @tagName(scope);
    const c_level = LogLevel.fromStd(level);

    var ctx: LogEmitter = .{
        .c_level = c_level,
        .scope_text = scope_text,
    };
    const writer: std.io.GenericWriter(
        *LogEmitter,
        error{},
        LogEmitter.write,
    ) = .{ .context = &ctx };

    nosuspend writer.print(format, args) catch {};
    ctx.flush();
}

/// Built-in log callback that writes to stderr.
///
/// Formats each message as "[level](scope): message\n". Can be passed
/// directly to ghostty_sys_set(GHOSTTY_SYS_OPT_LOG, &ghostty_sys_log_stderr).
///
/// Uses std.debug.lockStderrWriter for thread-safe, mutex-protected output.
/// On freestanding/wasm targets this is a no-op (no stderr available).
pub fn logStderr(
    _: ?*anyopaque,
    level: LogLevel,
    scope_ptr: [*]const u8,
    scope_len: usize,
    message_ptr: [*]const u8,
    message_len: usize,
) callconv(lib.calling_conv) void {
    if (comptime builtin.target.cpu.arch.isWasm()) return;

    const scope = scope_ptr[0..scope_len];
    const message = message_ptr[0..message_len];

    const level_text = switch (level) {
        .@"error" => "error",
        .warning => "warning",
        .info => "info",
        .debug => "debug",
    };

    var buffer: [64]u8 = undefined;
    const writer = std.debug.lockStderrWriter(&buffer);
    defer std.debug.unlockStderrWriter();
    nosuspend {
        if (scope.len > 0) {
            writer.print("[{s}]({s}): {s}\n", .{ level_text, scope, message }) catch {};
        } else {
            writer.print("[{s}]: {s}\n", .{ level_text, message }) catch {};
        }
    }
}

test "set decode_png with null clears" {
    // Start from a known state.
    global.decode_png = null;
    terminal_sys.decode_png = null;

    try std.testing.expectEqual(Result.success, set(.decode_png, null));
    try std.testing.expect(terminal_sys.decode_png == null);
}

test "set decode_png installs wrapper" {
    const S = struct {
        fn decode(_: ?*anyopaque, _: *const CAllocator, _: [*]const u8, _: usize, out: *Image) callconv(lib.calling_conv) bool {
            out.* = .{ .width = 1, .height = 1, .data = null, .data_len = 0 };
            return true;
        }
    };

    try std.testing.expectEqual(Result.success, set(
        .decode_png,
        @ptrCast(&S.decode),
    ));
    try std.testing.expect(terminal_sys.decode_png != null);

    // Clear it again.
    try std.testing.expectEqual(Result.success, set(.decode_png, null));
    try std.testing.expect(terminal_sys.decode_png == null);
}

test "set log with null clears" {
    global.log = null;

    try std.testing.expectEqual(Result.success, set(.log, null));
    try std.testing.expect(global.log == null);
}

test "set log installs callback" {
    const S = struct {
        var called: bool = false;
        fn logCb(_: ?*anyopaque, _: LogLevel, _: [*]const u8, _: usize, _: [*]const u8, _: usize) callconv(lib.calling_conv) void {
            called = true;
        }
    };

    try std.testing.expectEqual(Result.success, set(.log, @ptrCast(&S.logCb)));
    try std.testing.expect(global.log != null);

    emitLog(.info, "test", "hello");
    try std.testing.expect(S.called);

    // Clear it again.
    S.called = false;
    try std.testing.expectEqual(Result.success, set(.log, null));
    try std.testing.expect(global.log == null);

    emitLog(.info, "test", "should not arrive");
    try std.testing.expect(!S.called);
}

test "logFn small message single chunk" {
    const S = struct {
        var call_count: usize = 0;
        var total_len: usize = 0;

        fn logCb(_: ?*anyopaque, _: LogLevel, _: [*]const u8, _: usize, msg: [*]const u8, msg_len: usize) callconv(lib.calling_conv) void {
            _ = msg;
            call_count += 1;
            total_len += msg_len;
        }
    };

    S.call_count = 0;
    S.total_len = 0;
    global.log = @ptrCast(&S.logCb);
    defer {
        global.log = null;
    }

    logFn(.info, .default, "hello", .{});

    try std.testing.expectEqual(@as(usize, 1), S.call_count);
    try std.testing.expectEqual(@as(usize, 5), S.total_len);
}

test "logFn message exceeding chunk size is split" {
    const S = struct {
        var call_count: usize = 0;
        var total_len: usize = 0;

        fn logCb(_: ?*anyopaque, _: LogLevel, _: [*]const u8, _: usize, msg: [*]const u8, msg_len: usize) callconv(lib.calling_conv) void {
            _ = msg;
            call_count += 1;
            total_len += msg_len;
            // Each chunk must not exceed the buffer size.
            std.debug.assert(msg_len <= 2048);
        }
    };

    S.call_count = 0;
    S.total_len = 0;
    global.log = @ptrCast(&S.logCb);
    defer {
        global.log = null;
    }

    // Format a message larger than the 2048-byte buffer.
    // 'A' repeated 3000 times via a fill format.
    const fill: [3000]u8 = .{0x41} ** 3000;
    logFn(.info, .default, "{s}", .{@as([]const u8, &fill)});

    try std.testing.expect(S.call_count >= 2);
    try std.testing.expectEqual(@as(usize, 3000), S.total_len);
}

test "logFn message exactly at chunk boundary" {
    const S = struct {
        var call_count: usize = 0;
        var total_len: usize = 0;

        fn logCb(_: ?*anyopaque, _: LogLevel, _: [*]const u8, _: usize, msg: [*]const u8, msg_len: usize) callconv(lib.calling_conv) void {
            _ = msg;
            call_count += 1;
            total_len += msg_len;
            std.debug.assert(msg_len <= 2048);
        }
    };

    S.call_count = 0;
    S.total_len = 0;
    global.log = @ptrCast(&S.logCb);
    defer {
        global.log = null;
    }

    // Exactly 2048 bytes — should emit one full chunk, no remainder.
    const fill: [2048]u8 = .{0x42} ** 2048;
    logFn(.info, .default, "{s}", .{@as([]const u8, &fill)});

    try std.testing.expectEqual(@as(usize, 1), S.call_count);
    try std.testing.expectEqual(@as(usize, 2048), S.total_len);
}

test "logFn message exactly double chunk size" {
    const S = struct {
        var call_count: usize = 0;
        var total_len: usize = 0;

        fn logCb(_: ?*anyopaque, _: LogLevel, _: [*]const u8, _: usize, msg: [*]const u8, msg_len: usize) callconv(lib.calling_conv) void {
            _ = msg;
            call_count += 1;
            total_len += msg_len;
            std.debug.assert(msg_len <= 2048);
        }
    };

    S.call_count = 0;
    S.total_len = 0;
    global.log = @ptrCast(&S.logCb);
    defer {
        global.log = null;
    }

    // Exactly 4096 bytes — should emit exactly two full chunks.
    const fill: [4096]u8 = .{0x43} ** 4096;
    logFn(.info, .default, "{s}", .{@as([]const u8, &fill)});

    try std.testing.expectEqual(@as(usize, 2), S.call_count);
    try std.testing.expectEqual(@as(usize, 4096), S.total_len);
}

test "set userdata" {
    var data: u32 = 42;
    try std.testing.expectEqual(Result.success, set(.userdata, @ptrCast(&data)));
    try std.testing.expect(global.userdata == @as(?*anyopaque, @ptrCast(&data)));

    // Clear it.
    try std.testing.expectEqual(Result.success, set(.userdata, null));
    try std.testing.expect(global.userdata == null);
}
