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

const vaxis = @import("vaxis");
const xev = @import("xev");
const Cell = vaxis.Cell;

const networking = @import("./networking.zig");
const rotctl = @import("./rotctl.zig");

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

pub const panic = vaxis.panic_handler;

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

// these are all in degrees
pub const AzEl = struct { az: f64, el: f64 };

pub const RotInt = struct {
    allocator: std.mem.Allocator,
    // TODO: associate timestamps with this somehow
    offsets: AzEl = .{ .az = 0, .el = 0 },
    last_request: AzEl = .{ .az = 0, .el = 0 },
    current_posture: AzEl = .{ .az = 0, .el = 0 },

    // printbuffer: [128]u8 = undefined,
    termbuffer: std.io.BufferedWriter(4096, std.io.AnyWriter),
    vx: *vaxis.Vaxis,
    loop: *xev.Loop,

    fake: FakeRotator = .{},
    parser: rotctl.RotCtl = .{},
    server: networking.Server = .{},

    pub fn initFixed(self: *RotInt) void {
        self.server.rotint = self;
    }

    pub fn warn(_: *RotInt, comptime fmt: []const u8, args: anytype) void {
        log.warn(fmt, args);
    }

    pub fn forwardRequest(self: *RotInt, req: []const u8) void {
        var temp: [128]u8 = undefined;
        const command = self.parser.parseCommand(req) catch |err| switch (err) {
            error.Incomplete => return,
            error.NotSupported => {
                const response = (rotctl.RotReply{ .status = .not_supported }).write(temp[0..]) catch {
                    self.warn("serialization failure", .{});
                    return;
                };
                self.server.writeResponse(self.loop, response);
                return;
            },
            error.InvalidParameter => {
                const response = (rotctl.RotReply{ .status = .invalid_parameter }).write(temp[0..]) catch {
                    self.warn("serialization failure", .{});
                    return;
                };
                self.server.writeResponse(self.loop, response);
                return;
            },
        };

        // if command is `quit`, we should disconnect the client after our reply has been sent.
        const reply = self.fake.request(command);
        const response = reply.write(temp[0..]) catch {
            self.warn("serialization failure", .{});
            return;
        };
        self.server.writeResponse(self.loop, response);
    }

    fn draw(self: *RotInt) !void {
        const Static = struct {
            const lower_limit: u8 = 30;
            const next_ms: u64 = 8;
            var color_idx: u8 = lower_limit;
            var dir: enum { up, down } = .up;
        };

        const style: vaxis.Style = .{
            .fg = .{ .rgb = [_]u8{ Static.color_idx, Static.color_idx, Static.color_idx } },
        };

        const segment: vaxis.Segment = .{
            .text = "yeah ok",
            .style = style,
        };
        const win = self.vx.window();
        win.clear();

        const y_off = (win.height / 2) -| (6 / 2);
        const x_off = (win.width / 2) -| (30 / 2);
        const center = win.child(.{
            .x_off = x_off + win.x_off,
            .y_off = y_off + win.y_off,
            .width = .{ .limit = 30 },
            .height = .{ .limit = 6 },
            .border = .{ .where = .all, .style = style },
        });
        _ = try center.printSegment(segment, .{ .wrap = .grapheme });
        switch (Static.dir) {
            .up => {
                Static.color_idx += 1;
                if (Static.color_idx == 255) Static.dir = .down;
            },
            .down => {
                Static.color_idx -= 1;
                if (Static.color_idx == Static.lower_limit) Static.dir = .up;
            },
        }
        try self.vx.render(self.termbuffer.writer().any());
        try self.termbuffer.flush();
    }
};

const FakeRotator = struct {
    const park_position: AzEl = .{ .az = 180, .el = 90 };

    current: AzEl = park_position,

    fn request(self: *FakeRotator, command: rotctl.RotCommand) rotctl.RotReply {
        return switch (command) {
            .get_position => .{ .get_position = self.current },
            .set_position => |pos| blk: {
                self.current = pos;
                break :blk .{ .status = .okay };
            },
            .stop => .{ .status = .okay },
            .park => blk: {
                self.current = park_position;
                break :blk .{ .status = .okay };
            },
            .quit => .{ .status = .okay },
        };
    }
};

// const App = struct {
//     const lower_limit: u8 = 30;
//     const next_ms: u64 = 8;

//     allocator: std.mem.Allocator,
//     vx: *vaxis.Vaxis,
//     buffered_writer: std.io.BufferedWriter(4096, std.io.AnyWriter),
//     color_idx: u8,
//     dir: enum {
//         up,
//         down,
//     },
// };

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

    const alloc = gpa.allocator();

    var tty = try vaxis.Tty.init();
    defer tty.deinit();

    var vx = try vaxis.init(alloc, .{});
    defer vx.deinit(alloc, tty.anyWriter());

    var pool = xev.ThreadPool.init(.{});
    var loop = try xev.Loop.init(.{
        .thread_pool = &pool,
    });
    defer loop.deinit();

    var app: RotInt = .{
        .allocator = alloc,
        .termbuffer = tty.bufferedWriter(),
        .vx = &vx,
        .loop = &loop,
    };
    app.initFixed();

    var vx_loop: vaxis.xev.TtyWatcher(RotInt) = undefined;
    try vx_loop.init(&tty, &vx, &loop, &app, eventCallback);

    // try vx.enterAltScreen(tty.anyWriter());
    try vx.queryTerminalSend(tty.anyWriter());
    // Window size appears to be left uninitialized unless we manually set it here. This
    // seems sketchy to me (tty fd should be nonblocking for the event loop)
    const size = try vaxis.Tty.getWinsize(tty.fd);
    vx.resize(alloc, tty.anyWriter(), size) catch @panic("TODO");

    try app.server.listen(app.loop);

    try loop.run(.until_done);
}

fn eventCallback(
    ud: ?*RotInt,
    loop: *xev.Loop,
    watcher: *vaxis.xev.TtyWatcher(RotInt),
    event: vaxis.xev.Event,
) xev.CallbackAction {
    const app = ud orelse unreachable;
    switch (event) {
        .key_press => |key| keyp: {
            var mods = key.mods;
            mods.caps_lock = false;
            mods.num_lock = false;
            const scale: f64 = if (std.meta.eql(mods, .{ .shift = true })) 1 else 10;

            const delta: AzEl = switch (key.codepoint) {
                vaxis.Key.left, vaxis.Key.kp_left => .{ .az = -0.1 * scale, .el = 0 },
                vaxis.Key.right, vaxis.Key.kp_right => .{ .az = 0.1 * scale, .el = 0 },
                vaxis.Key.up, vaxis.Key.kp_up => .{ .az = 0, .el = 0.1 * scale },
                vaxis.Key.down, vaxis.Key.kp_down => .{ .az = 0 * scale, .el = -0.1 * scale },
                'l' => {
                    if (std.meta.eql(mods, .{ .ctrl = true }))
                        app.vx.queueRefresh();
                    break :keyp;
                },
                'c' => {
                    if (std.meta.eql(mods, .{ .ctrl = true })) {
                        loop.stop();
                        return .disarm;
                    }
                    break :keyp;
                },
                'q' => {
                    if (std.meta.eql(mods, .{})) {
                        loop.stop();
                        return .disarm;
                    }
                    break :keyp;
                },
                else => break :keyp,
            };

            _ = delta;

            // state.offsets.az += delta.az;
            // state.offsets.el += delta.el;
        },
        .winsize => |ws| {
            watcher.vx.resize(app.allocator, watcher.tty.anyWriter(), ws) catch
                return .disarm;
        },
        else => {},
    }
    return .rearm;
}

fn timerCallback(
    ud: ?*RotInt,
    loop: *xev.Loop,
    completion: *xev.Completion,
    err: xev.Timer.RunError!void,
) xev.CallbackAction {
    _ = err catch @panic("timer error");

    var app = ud orelse return .disarm;
    app.draw() catch @panic("couldn't draw");

    const timer = xev.Timer.init() catch unreachable;
    timer.run(loop, completion, 8, RotInt, app, timerCallback);
    return .disarm;
}
