//! DCC support

const std = @import("std");

const microzig = @import("microzig");
const rp2xxx = microzig.hal;
const peripherals = microzig.chip.peripherals;

const CV = @import("cv.zig").CV;

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

pub const Direction = enum {
    backward,
    forward,
};

pub const Speed = enum(u5) { stop, stopi, estop, estopi, step1, step2, step3, step4, step5, step6, step7, step8, step9, step10, step11, step12, step13, step14, step15, step16, step17, step18, step19, step20, step21, step22, step23, step24, step25, step26, step27, step28 };

const Packet = struct {
    address: u8,
    instruction: Instruction,

    /// Defined in S-9.2.1, Chapter 2.3
    const InstructionType = enum(u3) {
        decoder_control,
        advanced_operating,
        speed_reverse,
        speed_forward,
        function_group1,
        function_group2,
        feature_expansion,
        cv_access,
    };

    const Instruction = packed struct(u8) {
        speed: u4,
        headlight: u1, // lsb of speed
        itype: InstructionType,
    };

    pub fn speed(self: @This()) Speed {
        // TODO: Based on CV29 we should add support for 14 step speed control
        var ret: u5 = self.instruction.speed << 1;
        ret |= self.instruction.headlight;
        return @enumFromInt(ret);
    }
};

// TODO: extended address byte support (2.3)
pub const Parser = struct {
    const TR1_MIN_US = 52;
    // const TR1_MAX_US = 64;
    const TR1_MAX_US = 87;

    // zig fmt: off
    const MASK_PREAMBLE =  0b1111111111_0;
    const MASK_3BYTES =    0b1111111111_1_00000000_1_00000000_1_00000000_1;
    const COMPARE_3BYTES = 0b1111111111_0_00000000_0_00000000_0_00000000_1;
    const MASK_4BYTES =    0b1111111111_1_00000000_1_00000000_1_00000000_1_00000000_1;
    const COMPARE_4BYTES = 0b1111111111_0_00000000_0_00000000_0_00000000_0_00000000_1;
    const MASK_5BYTES =    0b1111111111_1_00000000_1_00000000_1_00000000_1_00000000_1_00000000_1;
    const COMPARE_5BYTES = 0b1111111111_0_00000000_0_00000000_0_00000000_0_00000000_0_00000000_1;
    // zig fmt: on

    const ParserState = enum {
        preamble,
        package,
    };

    const BitType = enum(u1) {
        zero,
        one,
    };

    const BUFFER_TYPE = u56; // 10+1 + 5x(8+1)

    state: ParserState = .preamble,
    time: microzig.drivers.time.Absolute = @enumFromInt(0),
    buffer: BUFFER_TYPE = 0,
    packets: std.fifo.LinearFifo(Packet, .{ .Static = 8 }) = std.fifo.LinearFifo(Packet, .{ .Static = 8 }).init(),

    pub fn signalRising(self: *@This()) void {
        self.time = rp2xxx.time.get_time_since_boot();
    }

    pub fn signalFalling(self: *@This()) void {
        const time_end = rp2xxx.time.get_time_since_boot();
        const diff = time_end.diff(self.time);

        // if (diff.less_than(@enumFromInt(TR1_MIN_US))) {
        //     log.debug("Diff {d} is too short, ignoring", .{diff.to_us()});
        //     self.time = rp2xxx.time.get_time_since_boot();
        //     return;
        // }

        // log.debug("DIFF: {d}", .{diff.to_us()});

        const bit: BitType = if (diff.less_than(@enumFromInt(TR1_MAX_US))) .one else .zero;
        switch (self.state) {
            .preamble => {
                switch (bit) {
                    .one => {
                        self.writeToBuffer(bit);
                    },
                    .zero => {
                        self.writeToBuffer(bit);
                        log.debug("DDC buffer: {b}", .{self.buffer});
                        if (self.buffer & MASK_PREAMBLE == MASK_PREAMBLE) {
                            self.state = .package;
                            log.debug("DDC state switched to: {}", .{self.state});
                        } else {
                            log.info("DDC preamble is invalid: {b}", .{self.buffer});
                            self.buffer = 0;
                        }
                    },
                }
            },
            .package => {
                self.writeToBuffer(bit);

                // TODO: consider using @clz before ckecking masks?

                // check for valid DCC packets
                if (self.buffer & @as(BUFFER_TYPE, MASK_3BYTES) ==
                    @as(BUFFER_TYPE, COMPARE_3BYTES))
                {
                    self.write3bytes();
                    self.reset();
                } else if (self.buffer & @as(BUFFER_TYPE, MASK_4BYTES) ==
                    @as(BUFFER_TYPE, COMPARE_4BYTES))
                {
                    log.debug("4 byte long DCC package detected!", .{});
                } else if (self.buffer & @as(BUFFER_TYPE, MASK_5BYTES) ==
                    @as(BUFFER_TYPE, COMPARE_5BYTES))
                {
                    log.debug("5 byte long DCC package detected!", .{});
                }
            },
        }
    }

    fn write3bytes(self: *@This()) void {
        log.debug("3 byte long DCC package detected!", .{});
        defer self.reset();

        const address: u8 = @truncate(self.buffer >> (2 * (8 + 1) + 1));
        const instruction: u8 = @truncate(self.buffer >> (8 + 1) + 1);
        const checksum: u8 = @truncate(self.buffer >> 1);

        if (address ^ instruction != checksum) {
            log.debug("DDC packet invalid, dropping it", .{});
            return;
        }

        const packet = Packet{
            .address = @bitCast(address),
            .instruction = @bitCast(instruction),
        };

        self.packets.writeItem(packet) catch {
            log.err("DDC packet FIFO full, dropping packet", .{});
            return;
        };

        log.debug("DCC packet added to FIFO", .{});
    }

    fn writeToBuffer(self: *@This(), bit: BitType) void {
        self.buffer <<= 1;
        self.buffer |= @intFromEnum(bit);
    }

    fn reset(self: *@This()) void {
        self.buffer = 0;
        self.state = .preamble;
    }
};

/// Helper functions for testing the DDC parser
pub const Test = struct {
    pub fn sendOne() void {
        peripherals.IO_BANK0.PROC0_INTF1.modify(.{ .GPIO15_EDGE_HIGH = 1 });
        rp2xxx.time.sleep_us(Parser.TIME_ONEMAX_US / 2);
        peripherals.IO_BANK0.PROC0_INTF1.modify(.{ .GPIO15_EDGE_LOW = 1 });
    }
    pub fn sendZero() void {
        peripherals.IO_BANK0.PROC0_INTF1.modify(.{ .GPIO15_EDGE_HIGH = 1 });
        rp2xxx.time.sleep_us(Parser.TIME_ONEMAX_US * 2);
        peripherals.IO_BANK0.PROC0_INTF1.modify(.{ .GPIO15_EDGE_LOW = 1 });
    }
    pub fn sendByte(bits: u8) void {
        for (0..8) |i| {
            const msb = bits >> (7 - @as(u3, @intCast(i)));
            if (@as(u1, @truncate(msb)) == 0) {
                sendZero();
            } else {
                sendOne();
            }
        }
    }
};