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 stderr = @import("../../os/stderr.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.enums.fromInt(Option, @intFromEnum(option)) orelse {
            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 {
    const buffer_size = 2048;

    c_level: LogLevel,
    scope_text: []const u8,
    writer: std.Io.Writer,

    fn drain(w: *std.Io.Writer, data: []const []const u8, splat: usize) std.Io.Writer.Error!usize {
        switch (data.len) {
            0 => unreachable,
            1 => if (splat == 0) return 0,
            else => {},
        }
        const self: *@This() = @fieldParentPtr("writer", w);
        if (w.end > 0) {
            emitLog(
                self.c_level,
                self.scope_text,
                w.buffer[0..w.end],
            );
        }
        const len: usize = @min(data[0].len, w.buffer.len);
        @memcpy(w.buffer[0..len], data[0][0..len]);
        w.end = len;
        return len;
    }
};

/// 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 log_emitter_buffer: [LogEmitter.buffer_size]u8 = undefined;
    var log_emitter: LogEmitter = .{
        .c_level = c_level,
        .scope_text = scope_text,
        .writer = .{
            .buffer = &log_emitter_buffer,
            .vtable = &.{
                .drain = LogEmitter.drain,
            },
        },
    };

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

/// 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).
///
/// Each log line is emitted with a single raw write to stderr, which keeps
/// concurrent log lines from interleaving. We intentionally avoid
/// `std.debug.lockStderr` because it routes through `std.Options.debug_io`
/// and would keep the entire `std.Io.Threaded` vtable alive in the binary
/// (see `os/stderr.zig`). 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",
    };

    // Large enough for a full logFn chunk plus the level/scope prefix.
    var buffer: [LogEmitter.buffer_size + 128]u8 = undefined;
    const line: ?[]const u8 = if (scope.len > 0)
        std.fmt.bufPrint(&buffer, "[{s}]({s}): {s}\n", .{ level_text, scope, message }) catch null
    else
        std.fmt.bufPrint(&buffer, "[{s}]: {s}\n", .{ level_text, message }) catch null;
    if (line) |v| {
        stderr.write(v);
        return;
    }

    // The line didn't fit in our buffer (an embedder called us directly
    // with a very large message). Fall back to writing the pieces
    // separately; interleaving with other threads is possible here but
    // this is a best-effort diagnostic path.
    stderr.write("[");
    stderr.write(level_text);
    stderr.write("]");
    if (scope.len > 0) {
        stderr.write("(");
        stderr.write(scope);
        stderr.write(")");
    }
    stderr.write(": ");
    stderr.write(message);
    stderr.write("\n");
}

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);
}
