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

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

const networking = @import("./networking.zig");
const rotctl = @import("./rotctl.zig");
const Config = @import("./Config.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 Log = struct {
    file: std.fs.File,

    pub fn init() !Log {
        var buf: [64]u8 = undefined;
        const now = std.time.milliTimestamp();
        const filename = try std.fmt.bufPrint(buf[0..], "rotint-{d}.eventlog", .{now});
        return .{
            .file = try std.fs.cwd().createFile(filename, .{ .exclusive = true }),
        };
    }

    pub fn writeEvent(self: Log, event: Event) void {
        var buf: [128]u8 = undefined;
        var fbs = std.io.fixedBufferStream(buf[0..]);
        const writer = fbs.writer().any();

        const now = std.time.milliTimestamp();
        writer.print("{d},", .{now}) catch return;
        event.write(writer) catch return;
        writer.writeByte('\n') catch return;
        self.file.writeAll(fbs.getWritten()) catch return;
    }

    pub const Event = union(enum) {
        offset_updated: AzEl,
        request_updated: AzEl,
        command_sent: AzEl,
        position_updated: AzEl,

        pub fn designator(self: Event) []const u8 {
            return switch (self) {
                .offset_updated => "O",
                .request_updated => "R",
                .command_sent => "C",
                .position_updated => "P",
            };
        }

        pub fn write(self: Event, writer: std.io.AnyWriter) !void {
            switch (self) {
                .offset_updated,
                .request_updated,
                .command_sent,
                .position_updated,
                => |pos| {
                    try writer.print(
                        "{s},{d:.1},{d:.1}",
                        .{ self.designator(), pos.az, pos.el },
                    );
                },
            }
        }
    };
};

pub const RotInt = struct {
    allocator: std.mem.Allocator,
    // TODO: associate timestamps with this somehow
    offsets: AzEl = .{ .az = 0, .el = 0 },
    requested_posture: AzEl = .{ .az = 0, .el = 0 },
    current_posture: AzEl = .{ .az = 0, .el = 0 },
    poll_interval: u64 = 500,
    command_freq: u8 = 4,
    pollcount: u9 = 0,
    state: State = .initial,
    conf: Config,

    termbuffer: std.io.BufferedWriter(4096, std.io.AnyWriter),
    vx: *vaxis.Vaxis,
    loop: *xev.Loop,

    parser: rotctl.RotCtl = .{},
    last_command: rotctl.RotCommand = undefined,
    server: networking.Server = .{},
    rotator: networking.Client = .{},
    poller: xev.Timer,
    poll_completion: xev.Completion = undefined,

    log: Log = undefined,

    pub const State = enum {
        initial,
        rotator_connected,
        rotator_ready,
        server_connected,
    };

    pub fn initInPlace(self: *RotInt) !void {
        self.server.rotint = self;
        self.rotator.rotint = self;
        self.log = try Log.init();

        const rotctld_addr = std.net.Address.parseIp(
            self.conf.rotctld.address,
            self.conf.rotctld.port,
        ) catch |err| {
            log.err("could not parse rotctld address {s}:{d}: {s}", .{
                self.conf.rotctld.address,
                self.conf.rotctld.port,
                @errorName(err),
            });
            return err;
        };
        try self.rotator.connect(self.loop, rotctld_addr);
    }

    pub fn stateEvent(self: *RotInt, event: State) void {
        switch (event) {
            .initial => {},
            .rotator_connected => if (self.state == .initial) {
                self.warn("rotator connected", .{});
                self.sendRotatorCommand(.get_position);
                self.state = .rotator_connected;
                self.vx.queueRefresh();
                self.draw() catch {};
            },
            .rotator_ready => if (self.state == .rotator_connected) {
                self.warn("rotator ready", .{});
                const listen_addr = std.net.Address.parseIp(
                    self.conf.listen.address,
                    self.conf.listen.port,
                ) catch |err| {
                    self.showError("bogus listen address: {s}", .{@errorName(err)});
                    return;
                };
                self.server.listen(self.loop, listen_addr) catch |err| {
                    self.showError("listen problem: {s}", .{@errorName(err)});
                    return;
                };
                // demangle here to avoid causing initial moves
                self.requested_posture = .{
                    .az = self.current_posture.az - self.offsets.az,
                    .el = self.current_posture.el - self.offsets.el,
                };
                self.state = .rotator_ready;
            },
            .server_connected => if (self.state == .rotator_ready) {
                self.warn("server listening", .{});
                self.state = .server_connected;
            },
        }
    }

    fn poll(
        self_: ?*RotInt,
        _: *xev.Loop,
        _: *xev.Completion,
        result: xev.Timer.RunError!void,
    ) xev.CallbackAction {
        const self = self_.?;
        result catch {
            self.warn("timer error???", .{});
            return .disarm;
        };

        // pre-increment so that this does not try to command when pollcount = 0 on the
        // first call
        self.pollcount = (self.pollcount + 1) % self.command_freq;

        if (self.pollcount == 0) {
            var mangled: AzEl = .{
                .az = self.requested_posture.az + self.offsets.az,
                .el = self.requested_posture.el,
            };
            mangled.el = if (mangled.el > 90)
                @max(
                    @min(
                        @min(mangled.el, 180 - self.conf.elevation_mask) - self.offsets.el,
                        180 - self.conf.elevation_mask,
                    ),
                    90,
                )
            else
                @min(
                    @max(
                        @max(mangled.el, self.conf.elevation_mask) + self.offsets.el,
                        self.conf.elevation_mask,
                    ),
                    90,
                );

            self.sendRotatorCommand(.{ .set_position = mangled });
        } else {
            self.sendRotatorCommand(.get_position);
        }

        return .disarm;
    }

    fn sendRotatorCommand(self: *RotInt, command: rotctl.RotCommand) void {
        self.last_command = command;
        self.rotator.sendCommand(self.loop, command);
    }

    pub fn handleRotatorReply(self: *RotInt, res: []const u8) void {
        const reply = self.parser.parseReplyFor(self.last_command, res) catch |err| switch (err) {
            error.Incomplete => return,
            error.InvalidParameter => {
                return;
            },
        };

        switch (reply) {
            .okay => if (self.last_command == .set_position)
                self.log.writeEvent(.{ .command_sent = self.last_command.set_position }),
            .get_position => |pos| {
                self.current_posture = pos;
                self.log.writeEvent(.{ .position_updated = pos });
                if (self.state == .rotator_connected) self.stateEvent(.rotator_ready);
                self.draw() catch {};
            },
            .status => |code| if (code != .okay)
                self.warn("rotctl error {s}", .{@tagName(code)}),
        }
        self.poller.run(self.loop, &self.poll_completion, self.poll_interval, RotInt, self, poll);
    }

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

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

    pub fn handleControlRequest(self: *RotInt, req: []const u8) void {
        const command = self.parser.parseCommand(req) catch |err| switch (err) {
            error.Incomplete => return,
            error.NotSupported => {
                self.server.respond(self.loop, .{ .status = .not_supported });
                return;
            },
            error.InvalidParameter => {
                self.server.respond(self.loop, .{ .status = .invalid_parameter });
                return;
            },
        };

        switch (command) {
            .get_position => self.server.respond(self.loop, .{ .get_position = self.current_posture }),

            .set_position => |pos| {
                self.requested_posture = pos;
                self.server.respond(self.loop, .okay);
                self.log.writeEvent(.{ .request_updated = pos });
                self.draw() catch {};
            },

            .stop => self.server.respond(self.loop, .okay),
            .park => self.server.respond(self.loop, .okay),
            .quit => {
                self.server.respond(self.loop, .okay);
                self.server.should_disconnect = true;
            },
        }
    }

    fn draw(self: *RotInt) !void {
        const win = self.vx.window();
        win.clear();

        var lines: [4][128]u8 = undefined;
        const offsets: vaxis.Segment = .{ .text = try std.fmt.bufPrint(
            lines[0][0..],
            "Offsets:   Az: {d: >6.1}, El: {d: >6.1}",
            .{ self.offsets.az, self.offsets.el },
        ) };
        const requested: vaxis.Segment = .{ .text = try std.fmt.bufPrint(
            lines[1][0..],
            "Requested: Az: {d: >6.1}, El: {d: >6.1}",
            .{ self.requested_posture.az, self.requested_posture.el },
        ) };
        const current: vaxis.Segment = .{ .text = try std.fmt.bufPrint(
            lines[2][0..],
            "Current:   Az: {d: >6.1}, El: {d: >6.1}",
            .{ self.current_posture.az, self.current_posture.el },
        ) };
        const pollinfo: vaxis.Segment = .{ .text = try std.fmt.bufPrint(
            lines[3][0..],
            "Poll: {d} ms, Command: {d} ms",
            .{ self.poll_interval, self.command_freq * self.poll_interval },
        ) };

        const center = vaxis.widgets.alignment.center(win, offsets.text.len, 1);
        _ = try center.printSegment(offsets, .{});
        const center_up = win.initChild(center.x_off, center.y_off - 1, .{ .limit = requested.text.len }, .{ .limit = 1 });
        _ = try center_up.printSegment(requested, .{});
        const center_down = win.initChild(center.x_off, center.y_off + 1, .{ .limit = current.text.len }, .{ .limit = 1 });
        _ = try center_down.printSegment(current, .{});
        const poll_win = win.initChild(center.x_off, center.y_off + 3, .{ .limit = pollinfo.text.len }, .{ .limit = 1 });
        _ = try poll_win.printSegment(pollinfo, .{});

        const help: vaxis.Segment = .{
            .text = "Keys: q/^c - quit, ↑/↓ - el +/- 1° (+shift - 0.1°), ←/→ - az -/+ 1° (+shift - 0.1°), ⏎ - reset offsets, ^l - redraw, w/a/s/d - poll rates",
            .style = .{ .fg = .{ .index = 245 } },
        };

        const helpwin = win.child(.{ .x_off = 0, .y_off = 0 });
        _ = try helpwin.printSegment(help, .{ .wrap = .word });

        try self.vx.render(self.termbuffer.writer().any());
        try self.termbuffer.flush();
    }

    fn terminalEvent(
        self_: ?*RotInt,
        loop: *xev.Loop,
        watcher: *vaxis.xev.TtyWatcher(RotInt),
        event: vaxis.xev.Event,
    ) xev.CallbackAction {
        const self = self_.?;
        switch (event) {
            .key_press => |key| {
                var mods = key.mods;
                mods.caps_lock = false;
                mods.num_lock = false;

                switch (key.codepoint) {
                    vaxis.Key.left,
                    vaxis.Key.kp_left,
                    vaxis.Key.right,
                    vaxis.Key.kp_right,
                    vaxis.Key.up,
                    vaxis.Key.kp_up,
                    vaxis.Key.down,
                    vaxis.Key.kp_down,
                    vaxis.Key.enter,
                    vaxis.Key.kp_enter,
                    => |arrow| {
                        const scale: f64 = if (std.meta.eql(mods, .{ .shift = true })) 1 else 10;
                        const delta: AzEl = switch (arrow) {
                            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 },
                            vaxis.Key.enter, vaxis.Key.kp_enter => .{ .az = -self.offsets.az, .el = -self.offsets.el },
                            else => unreachable,
                        };

                        self.offsets.az += delta.az;
                        self.offsets.el += delta.el;
                        self.log.writeEvent(.{ .offset_updated = self.offsets });
                        self.draw() catch {
                            self.warn("draw failure", .{});
                        };
                    },
                    'w', 'a', 's', 'd' => |wasd| {
                        switch (wasd) {
                            'w' => self.poll_interval += 100,
                            'a' => if (self.command_freq > 2) {
                                self.command_freq -= 1;
                            },
                            's' => if (self.poll_interval > 100) {
                                self.poll_interval -= 100;
                            },
                            'd' => self.command_freq += 1,
                            else => unreachable,
                        }
                        self.draw() catch {};
                    },
                    'l' => if (std.meta.eql(mods, .{ .ctrl = true })) {
                        self.vx.queueRefresh();
                        self.draw() catch {};
                    },
                    'c' => if (std.meta.eql(mods, .{ .ctrl = true })) {
                        loop.stop();
                        return .disarm;
                    },
                    'q' => if (std.meta.eql(mods, .{})) {
                        loop.stop();
                        return .disarm;
                    },
                    else => {},
                }
            },
            .winsize => |ws| {
                watcher.vx.resize(self.allocator, watcher.tty.anyWriter(), ws) catch
                    return .disarm;
            },
            else => {},
        }
        return .rearm;
    }
};

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 env = try std.process.getEnvMap(alloc);
    defer env.deinit();

    var app: RotInt = .{
        .allocator = alloc,
        .conf = Config.load(alloc, &env) catch cfg: {
            log.warn("Could not load config file. Using defaults.", .{});
            break :cfg Config.default();
        },
        .termbuffer = tty.bufferedWriter(),
        .vx = &vx,
        .loop = &loop,
        .poller = try xev.Timer.init(),
    };
    try app.initInPlace();

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

    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 loop.run(.until_done);
}
