const std = @import("std");
const testing = std.testing;
const Allocator = std.mem.Allocator;
const lib = @import("../lib.zig");
const CAllocator = lib.alloc.Allocator;
const sgr = @import("../sgr.zig");
const Result = @import("result.zig").Result;

const log = std.log.scoped(.sgr);

/// Wrapper around parser that tracks the allocator for C API usage.
const ParserWrapper = struct {
    parser: sgr.Parser,
    alloc: Allocator,
};

/// C: GhosttySgrParser
pub const Parser = ?*ParserWrapper;

pub fn new(
    alloc_: ?*const CAllocator,
    result: *Parser,
) callconv(lib.calling_conv) Result {
    const alloc = lib.alloc.default(alloc_);
    const ptr = alloc.create(ParserWrapper) catch
        return .out_of_memory;
    ptr.* = .{
        .parser = .empty,
        .alloc = alloc,
    };
    result.* = ptr;
    return .success;
}

pub fn free(parser_: Parser) callconv(lib.calling_conv) void {
    const wrapper = parser_ orelse return;
    const alloc = wrapper.alloc;
    const parser: *sgr.Parser = &wrapper.parser;
    if (parser.params.len > 0) alloc.free(parser.params);
    alloc.destroy(wrapper);
}

pub fn reset(parser_: Parser) callconv(lib.calling_conv) void {
    const wrapper = parser_ orelse return;
    const parser: *sgr.Parser = &wrapper.parser;
    parser.idx = 0;
}

pub fn setParams(
    parser_: Parser,
    params: [*]const u16,
    seps_: ?[*]const u8,
    len: usize,
) callconv(lib.calling_conv) Result {
    const wrapper = parser_ orelse return .invalid_value;
    const alloc = wrapper.alloc;
    const parser: *sgr.Parser = &wrapper.parser;

    // Copy our new parameters
    const params_slice = alloc.dupe(u16, params[0..len]) catch
        return .out_of_memory;
    if (parser.params.len > 0) alloc.free(parser.params);
    parser.params = params_slice;

    // If we have separators, set that state too.
    parser.params_sep = .initEmpty();
    if (seps_) |seps| {
        if (len > @TypeOf(parser.params_sep).bit_length) {
            log.warn("ghostty_sgr_set_params: separators length {} exceeds max supported length {}", .{
                len,
                @TypeOf(parser.params_sep).bit_length,
            });
            return .invalid_value;
        }

        for (seps[0..len], 0..) |sep, i| {
            if (sep == ':') parser.params_sep.set(i);
        }
    }

    // Reset our parsing state
    parser.idx = 0;

    return .success;
}

pub fn next(
    parser_: Parser,
    result: *sgr.Attribute.C,
) callconv(lib.calling_conv) bool {
    const wrapper = parser_ orelse return false;
    const parser: *sgr.Parser = &wrapper.parser;
    if (parser.next()) |attr| {
        result.* = attr.cval();
        return true;
    }

    return false;
}

pub fn unknown_full(
    unknown: sgr.Attribute.Unknown.C,
    ptr: ?*[*]const u16,
) callconv(lib.calling_conv) usize {
    if (ptr) |p| p.* = unknown.full_ptr;
    return unknown.full_len;
}

pub fn unknown_partial(
    unknown: sgr.Attribute.Unknown.C,
    ptr: ?*[*]const u16,
) callconv(lib.calling_conv) usize {
    if (ptr) |p| p.* = unknown.partial_ptr;
    return unknown.partial_len;
}

pub fn attribute_tag(
    attr: sgr.Attribute.C,
) callconv(lib.calling_conv) sgr.Attribute.Tag {
    return attr.tag;
}

pub fn attribute_value(
    attr: *sgr.Attribute.C,
) callconv(lib.calling_conv) *sgr.Attribute.CValue {
    return &attr.value;
}

pub fn wasm_alloc_attribute() callconv(lib.calling_conv) *sgr.Attribute.C {
    const alloc = std.heap.wasm_allocator;
    const ptr = alloc.create(sgr.Attribute.C) catch @panic("out of memory");
    return ptr;
}

pub fn wasm_free_attribute(attr: *sgr.Attribute.C) callconv(lib.calling_conv) void {
    const alloc = std.heap.wasm_allocator;
    alloc.destroy(attr);
}

test "alloc" {
    var p: Parser = undefined;
    try testing.expectEqual(Result.success, new(
        &lib.alloc.test_allocator,
        &p,
    ));
    free(p);
}

test "simple params, no seps" {
    var p: Parser = undefined;
    try testing.expectEqual(Result.success, new(
        &lib.alloc.test_allocator,
        &p,
    ));
    defer free(p);

    try testing.expectEqual(Result.success, setParams(
        p,
        &.{1},
        null,
        1,
    ));

    // Set it twice on purpose to make sure we don't leak.
    try testing.expectEqual(Result.success, setParams(
        p,
        &.{1},
        null,
        1,
    ));

    // Verify we get bold
    var attr: sgr.Attribute.C = undefined;
    try testing.expect(next(p, &attr));
    try testing.expectEqual(.bold, attr.tag);

    // Nothing else
    try testing.expect(!next(p, &attr));
}
