const std = @import("std");
const builtin = @import("builtin");
const kindle = @import("kindle");
const clap = @import("clap");

const protocol = @import("eink_feed_protocol");

const client = @import("root.zig");

pub const std_options: std.Options = .{
    .log_level = if (builtin.mode == .Debug) .debug else .info,
};

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

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{
        .thread_safe = true,
    }){};
    defer _ = gpa.deinit();
    const allocator = gpa.allocator();

    {
        const params = comptime clap.parseParamsComptime(
            \\-h, --help        Display this help and exit.
            \\--host <str>      eink-feed server host (e.g. hostname:port).
            \\--name <str>      Client name.
            \\--partial         Enable partial eink framebuffer updates. May not look as sharp but reduces flickering.
        );

        var diag = clap.Diagnostic{};
        var res = clap.parse(clap.Help, &params, clap.parsers.default, .{
            .diagnostic = &diag,
            .allocator = allocator,
        }) catch |err| {
            diag.report(std.io.getStdErr().writer(), err) catch {};
            return err;
        };
        defer res.deinit();

        if (res.args.help != 0 or
            res.args.host == null or
            res.args.name == null)
        {
            const writer = std.io.getStdErr().writer();
            try writer.writeAll("eink-feed-client\n");
            return clap.help(writer, clap.Help, &params, .{});
        }

        const client_name: []const u8 = res.args.name.?;
        const ws_url = try std.fmt.allocPrint(allocator, "{s}/api/ws/display", .{res.args.host.?});
        defer allocator.free(ws_url);

        var fb = try kindle.eink.fb.Framebuffer.open("/dev/fb0");
        defer fb.deinit();
        try fb.clear();

        var renderer = client.Renderer.init(allocator, &fb, res.args.partial != 0);

        var cb_writer = FeedCallbackWriter.init(allocator, &renderer);
        defer cb_writer.deinit();
        var ws = client.WebsocketClient.init(
            allocator,
            .{ .url = ws_url },
            cb_writer.writer(),
        );
        defer ws.deinit();

        var event_callback = FeedEventCallback.init(allocator, &ws);
        var input_listener = try client.InputListener.init(
            allocator,
            &.{ "/dev/input/event0", "/dev/input/event1" },
            event_callback.listener(),
        );
        defer input_listener.deinit();
        try input_listener.start();

        try ws.connect(client_name);
    }
}

const FeedEventCallback = struct {
    const Self = @This();

    allocator: std.mem.Allocator,
    ws: *client.WebsocketClient,

    pub fn init(allocator: std.mem.Allocator, ws: *client.WebsocketClient) Self {
        return .{
            .allocator = allocator,
            .ws = ws,
        };
    }

    fn listener(self: *Self) client.InputListener.EventCallback {
        return client.InputListener.EventCallback.init(self);
    }

    pub fn keyEventCallback(self: *const Self, device: []const u8, key_code: u32, key_name: []const u8, value: u32) !void {
        _ = device;

        if (!std.mem.startsWith(u8, key_name, "KEY_")) return;
        const trim_name = key_name[4..];

        const key_update = protocol.protobuf.KeyUpdate{
            .key_code = key_code,
            .key_name = .{ .Const = trim_name },
            .value = value,
        };
        const msg = protocol.protobuf.ClientMessage{
            .key_update = .{ .key_update = key_update },
        };

        const buf = try msg.encode(self.allocator);
        defer self.allocator.free(buf);

        try self.ws.sendMessage(buf);
    }
};

const FeedCallbackWriter = struct {
    const Self = @This();

    allocator: std.mem.Allocator,
    data: ?[]u8,

    renderer: *client.Renderer,

    pub fn init(allocator: std.mem.Allocator, renderer: *client.Renderer) Self {
        return .{
            .allocator = allocator,
            .data = null,
            .renderer = renderer,
        };
    }

    pub fn deinit(self: Self) void {
        if (self.data) |data| self.allocator.free(data);
    }

    fn handleMsg(self: *const Self, buf: []const u8) !void {
        const msg = try protocol.protobuf.ServerMessage.decode(buf, self.allocator);
        defer msg.deinit();

        if (msg.frame) |frame_pb| {
            const frame = protocol.models.Frame{
                .display = protocol.models.Display.fromProtobuf(frame_pb.display.?),
                .pixels = frame_pb.pixels.getSlice(),
            };

            log.info(
                "Received frame update: w = {d}, h = {d}, orientation = {s}",
                .{ frame.display.width, frame.display.height, frame.display.orientation.toString() },
            );

            var image = try client.Renderer.imageFromFrame(self.allocator, &frame);
            defer image.deinit();
            var fixed_image = try self.renderer.fixImageForFb(&frame.display, &image);
            defer fixed_image.deinit();
            try self.renderer.render(&fixed_image);
        }
    }

    pub fn writeCallback(self: *Self, buf: []const u8, meta: *const client.WebsocketClient.WriterCallback.Meta) !void {
        if (self.data) |data| {
            if (meta.bytesleft == 0) {
                defer {
                    self.allocator.free(data);
                    self.data = null;
                }
                const offset: usize = @intCast(meta.offset);
                @memcpy(data[offset .. offset + buf.len], buf);
                try self.handleMsg(data);
            } else {
                const offset: usize = @intCast(meta.offset);
                @memcpy(data[offset .. offset + buf.len], buf);
            }
        } else if (meta.bytesleft == 0) {
            try self.handleMsg(buf);
        } else {
            const size: usize = buf.len + @as(usize, @intCast(meta.bytesleft));
            self.data = try self.allocator.alloc(u8, size);
            @memcpy(self.data.?[0..buf.len], buf);
        }
    }

    fn writer(self: *Self) client.WebsocketClient.WriterCallback {
        return client.WebsocketClient.WriterCallback.init(self);
    }
};
