const std = @import("std");
const AzEl = @import("./main.zig").AzEl;

const TokenIter: type = std.mem.TokenIterator(u8, .scalar);

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

pub const RotCtl = struct {
    // commands
    writebuf: [128]u8 = undefined,
    wlen: usize = 0,
    // replies
    readbuf: [128]u8 = undefined,
    rlen: usize = 0,
    // correlate a reply to a command
    // last_command: ?RotCommand = null,

    const ParseError = error{Incomplete};

    pub fn parseCommand(self: *RotCtl, incoming: []const u8) (ParseError || RotCommand.ParseError)!RotCommand {
        @memcpy(self.writebuf[self.wlen .. self.wlen + incoming.len], incoming);
        self.wlen += incoming.len;

        const end = std.mem.indexOfScalarPos(u8, self.writebuf[0..], 0, '\n') orelse
            return error.Incomplete;

        defer {
            self.wlen = shiftBuf(&self.writebuf[0..self.wlen], end + 1);
        }
        return try RotCommand.parse(self.writebuf[0..end]);
    }

    pub fn parseReplyFor(self: *RotCtl, command: RotCommand, incoming: []const u8) (ParseError || RotReply.ParseError)!RotReply {
        @memcpy(self.readbuf[self.rlen .. self.rlen + incoming.len], incoming);
        self.rlen += incoming.len;

        var lines: [3][]const u8 = undefined;
        var lslice: [][]const u8 = lines[0..0];
        var offset: usize = 0;
        while (std.mem.indexOfScalarPos(u8, self.readbuf[0..], offset, '\n')) |idx| {
            const line = self.readbuf[offset..idx];
            offset += idx + 1;
            lslice.len += 1;
            lslice[lslice.len - 1] = line;
            if (lslice.len == command.expectedResponseLines() or (std.mem.startsWith(u8, line, "RPRT ") and line.len > 5)) {
                defer {
                    self.rlen = shiftBuf(&self.readbuf[0..self.rlen], offset);
                }
                return RotReply.parse(command, lslice);
            }
        } else return error.Incomplete;
    }

    fn shiftBuf(buf: *const []u8, end: usize) usize {
        if (end < buf.len)
            std.mem.copyForwards(u8, buf.*[0..], buf.*[end..buf.len]);

        return buf.len -| end;
    }
};

// consider: if we want to support passthrough, then RotCommand needs to also have space
// to store the line to pass through, since it can't store a slice to any of our
// existing buffers (those potentially get invalidated immediately after parsing). We
// don't benefit from this much, since we will not be doing direct passthrough.
//
// pub const RotCommand = struct {
//     buf: [128]u8,
//     command: union(enum) {
//         invalid: Status,
//         set_position: AzEl,
//         passthrough: usize, // length of passthrough string in buf
//     },
// };

pub const RotCommand = union(enum) {
    get_position,
    set_position: AzEl,
    stop,
    park,
    quit,

    const ParseError = error{
        NotSupported,
        InvalidParameter,
    };

    pub fn write(self: RotCommand, buf: []u8) ![]u8 {
        return switch (self) {
            .get_position => try writeLiteral(buf, "p\n"),
            .set_position => |pos| try std.fmt.bufPrint(
                buf,
                "P {d:.1} {d:.1}\n",
                .{ pos.az, pos.el },
            ),
            .stop => try writeLiteral(buf, "S\n"),
            .park => try writeLiteral(buf, "K\n"),
            .quit => try writeLiteral(buf, "q\n"),
        };
    }

    pub fn expectedResponseLines(self: RotCommand) usize {
        return switch (self) {
            .get_position => 2,
            .set_position, .stop, .park, .quit => 1,
        };
    }

    fn writeLiteral(buf: []u8, lit: []const u8) error{NoSpaceLeft}![]u8 {
        if (buf.len < lit.len) return error.NoSpaceLeft;

        @memcpy(buf[0..lit.len], lit);
        return buf[0..lit.len];
    }

    pub fn parse(line: []const u8) ParseError!RotCommand {
        var tok = std.mem.tokenizeScalar(u8, line, ' ');
        // the first token is only null if the line is empty, and we've already
        // guaranteed it isn't.
        const first = tok.next() orelse unreachable;

        if (first.len == 1) {
            return switch (first[0]) {
                'p' => parseNoArgs(&tok, .get_position),
                'P' => parseSetPosition(&tok),
                'S' => parseNoArgs(&tok, .stop),
                'K' => parseNoArgs(&tok, .park),
                'q', 'Q' => parseNoArgs(&tok, .quit),
                else => error.NotSupported,
            };
        } else for (longs) |spec| {
            if (std.mem.eql(u8, spec.name, first))
                return spec.parse(&tok);
        } else return error.NotSupported;
    }

    fn parseSetPosition(tok: *TokenIter) ParseError!RotCommand {
        const azstr = tok.next() orelse return error.InvalidParameter;
        const elstr = tok.next() orelse return error.InvalidParameter;
        if (tok.next() != null) return error.InvalidParameter;

        return .{ .set_position = .{
            .az = std.fmt.parseFloat(f64, azstr) catch return error.InvalidParameter,
            .el = std.fmt.parseFloat(f64, elstr) catch return error.InvalidParameter,
        } };
    }

    fn parseNoArgs(tok: *TokenIter, ret: RotCommand) ParseError!RotCommand {
        return if (tok.next() != null) error.InvalidParameter else ret;
    }

    const SpaceTokenParser = *const fn (tok: *TokenIter) ParseError!RotCommand;
    const LongSpec = struct { name: []const u8, parse: SpaceTokenParser };
    const longs = [_]LongSpec{
        .{ .name = "get_pos", .parse = partialParseNoArgs(.get_position) },
        .{ .name = "set_pos", .parse = parseSetPosition },
        .{ .name = "stop", .parse = partialParseNoArgs(.stop) },
        .{ .name = "park", .parse = partialParseNoArgs(.park) },
    };

    fn partialParseNoArgs(comptime res: RotCommand) SpaceTokenParser {
        return struct {
            fn thunk(tok: *TokenIter) ParseError!RotCommand {
                return parseNoArgs(tok, res);
            }
        }.thunk;
    }
};

pub const RotReply = union(enum) {
    okay,
    get_position: AzEl,
    status: Status,

    pub const ParseError = error{
        InvalidParameter,
    } || Status.ParseError;

    pub fn parse(command: RotCommand, lines: []const []const u8) ParseError!RotReply {
        return switch (command) {
            .set_position, .park, .stop, .quit => switch (lines.len) {
                1 => blk: {
                    const code = try Status.parse(lines[0]);
                    break :blk if (code == .okay) .okay else .{ .status = code };
                },
                else => error.InvalidParameter,
            },

            .get_position => switch (lines.len) {
                1 => .{ .status = try Status.parse(lines[0]) },
                2 => .{ .get_position = .{
                    .az = std.fmt.parseFloat(f64, lines[0]) catch return error.InvalidParameter,
                    .el = std.fmt.parseFloat(f64, lines[1]) catch return error.InvalidParameter,
                } },
                else => error.InvalidParameter,
            },
        };
    }

    pub fn write(self: RotReply, buf: []u8) ![]const u8 {
        return switch (self) {
            .get_position => |pos| try std.fmt.bufPrint(buf, "{d:.1}\n{d:.1}\n", .{ pos.az, pos.el }),
            .okay => try Status.okay.write(buf),
            .status => |code| try code.write(buf),
        };
    }
};

pub const Status = enum(i8) {
    okay = 0,
    invalid_parameter = 1,
    invalid_configuration = 2,
    out_of_memory = 3,
    not_implemented = 4,
    timeout = 5,
    io_error = 6,
    internal_error = 7,
    protocol_error = 8,
    command_rejected = 9,
    parameter_truncated = 10,
    not_supported = 11,
    not_targetable = 12,
    bus_error = 13,
    bus_busy = 14,
    invalid_arg = 15,
    invalid_vfo = 16,
    domain_error = 17,
    deprecated = 18,
    security = 19,
    power = 20,
    // make this nonexhaustive for kicks
    _,

    const ParseError = error{InvalidParameter};

    pub fn parse(buf: []const u8) ParseError!Status {
        if (!std.mem.startsWith(u8, buf, "RPRT ") or !(buf.len > 5))
            return error.InvalidParameter;

        const code = std.fmt.parseInt(i8, buf[5..], 10) catch
            return error.InvalidParameter;

        return @enumFromInt(-code);
    }

    pub fn write(self: Status, buf: []u8) ![]u8 {
        if (buf.len < 10) return error.NoSpaceLeft;
        return self.replyFrame(buf[0..10]);
    }

    pub fn format(self: Status, comptime fmt: []const u8, options: std.fmt.FormatOptions, writer: anytype) !void {
        _ = fmt;
        _ = options;
        try writer.print("RPRT {d}\n", .{-@intFromEnum(self)});
    }

    pub fn replyFrame(self: Status, buf: *[10]u8) []u8 {
        return std.fmt.bufPrint(buf, "{}", .{self}) catch unreachable;
    }
};
