const std = @import("std");

const c = @cImport({
    @cInclude("curl/curl.h");
});

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

const Self = @This();

pub const ConnectOptions = struct {
    url: []const u8,
    wss: bool = false,
    verbose: bool = false,
};

pub const WriterCallback = struct {
    pub const Meta = c.curl_ws_frame;

    ptr: *anyopaque,
    write_callback_fn: *const fn (ptr: *anyopaque, data: []const u8, meta: *const Meta) anyerror!void,

    pub fn init(ptr: anytype) WriterCallback {
        const T = @TypeOf(ptr);
        const ptr_info = @typeInfo(T);
        if (ptr_info != .pointer) @compileError("ptr must be a pointer");
        if (ptr_info.pointer.size != .one) @compileError("ptr must be a single item pointer");

        const gen = struct {
            pub fn writeCallback(pointer: *anyopaque, data: []const u8, meta: *const Meta) anyerror!void {
                const self: T = @ptrCast(@alignCast(pointer));
                try @call(
                    .always_inline,
                    ptr_info.pointer.child.writeCallback,
                    .{ self, data, meta },
                );
            }
        };

        return .{
            .ptr = ptr,
            .write_callback_fn = gen.writeCallback,
        };
    }

    pub fn writeCallback(self: WriterCallback, data: []const u8, meta: *const Meta) !void {
        return self.write_callback_fn(self.ptr, data, meta);
    }
};

allocator: std.mem.Allocator,
connect_options: ConnectOptions,
callback_writer: WriterCallback,
curl: ?*c.CURL = null,
send_lock: std.Thread.Mutex = .{},

pub fn init(
    allocator: std.mem.Allocator,
    connect_options: ConnectOptions,
    callback_writer: WriterCallback,
) Self {
    return .{
        .allocator = allocator,
        .connect_options = connect_options,
        .callback_writer = callback_writer,
    };
}

pub const CurlError = error{
    curl,
};

fn handleCurlCode(result: c.CURLcode) CurlError!void {
    if (result != c.CURLE_OK) {
        log.err("curl_easy_perform() failed: {s} ({d})", .{ c.curl_easy_strerror(result), result });
        return CurlError.curl;
    }
}

const CallbackData = struct {
    curl: *c.CURL,
    writer: *const WriterCallback,
};

fn writeCallback(data: [*c]u8, size: usize, nitems: usize, clientp: *anyopaque) callconv(.C) c_ulong {
    const cb_data = @as(*CallbackData, @ptrCast(@alignCast(clientp)));

    const realsize = nitems * size;
    const buf = data[0..realsize];

    const meta = c.curl_ws_meta(cb_data.curl).?;
    cb_data.writer.writeCallback(buf, meta) catch |err| {
        log.err("unrecoverable error during protobuf decoding: {}", .{err});
        unreachable;
    };

    return realsize;
}

pub fn sendMessage(self: *Self, data: []const u8) !void {
    if (self.curl) |curl| {
        self.send_lock.lock();
        defer self.send_lock.unlock();

        var sent: usize = undefined;
        try handleCurlCode(
            c.curl_ws_send(curl, @ptrCast(data), data.len, &sent, 0, c.CURLWS_TEXT),
        );
    }
}

pub fn deinit(self: *Self) void {
    self.close();
    if (self.curl) |curl| {
        c.curl_easy_cleanup(curl);
    }
}

pub fn close(self: *Self) void {
    if (self.curl) |curl| {
        var sent: usize = undefined;
        _ = c.curl_ws_send(curl, &{}, 0, &sent, 0, c.CURLWS_CLOSE);
    }
}

pub fn connect(self: *Self, client_name: []const u8) !void {
    self.close();

    const curl: *c.CURL = c.curl_easy_init().?;
    self.curl = curl;

    const escaped_client_name = blk: {
        const s: ?[*:0]u8 = c.curl_easy_escape(curl, @ptrCast(client_name), @intCast(client_name.len));
        if (s == null) {
            return CurlError.curl;
        }
        break :blk s.?;
    };
    defer c.curl_free(escaped_client_name);

    const url = try std.fmt.allocPrint(self.allocator, "{s}://{s}?name={s}", .{
        if (self.connect_options.wss) "wss" else "ws",
        self.connect_options.url,
        escaped_client_name,
    });
    defer self.allocator.free(url);

    try handleCurlCode(
        c.curl_easy_setopt(curl, c.CURLOPT_URL, @as([*:0]u8, @ptrCast(url))),
    );
    if (self.connect_options.verbose) {
        try handleCurlCode(
            c.curl_easy_setopt(curl, c.CURLOPT_VERBOSE, @as(c_long, 1)),
        );
    }

    const cb_data = CallbackData{
        .curl = curl,
        .writer = &self.callback_writer,
    };
    try handleCurlCode(
        c.curl_easy_setopt(curl, c.CURLOPT_WRITEFUNCTION, writeCallback),
    );
    try handleCurlCode(c.curl_easy_setopt(curl, c.CURLOPT_WRITEDATA, &cb_data));

    try handleCurlCode(
        c.curl_easy_perform(curl),
    );
}
