//! shared definitions between the different PS/2 drivers
const std = @import("std");
const runtime = @import("runtime");
const acpi_ids = @import("acpi-ids");
const device = runtime.device;
const system = runtime.system;

/// PS-2 io ports:
/// The PS/2 Controller itself uses 2 IO ports (usually, IO ports 0x60 and 0x64). Like many IO
///  ports, reads and writes may access different internal registers.
///
/// Historical note: The PC-XT PPI had used port 0x61 to reset the keyboard interrupt request
/// signal (among other unrelated functions). Port 0x61 has no keyboard related functions on AT and
/// PS/2 compatibles.
///
/// The Data Port (typically IO Port 0x60) is used for reading data that was received from a PS/2
/// device or from the PS/2 controller itself and writing data to a PS/2 device or to the PS/2
/// controller itself.
// Access type: Read/Write
pub const dataPort = 0x60;
// Access type: Read
pub const statusRegisterPort = 0x64;
// Access type: Write
pub const CommandRegisterPort = 0x64;

/// How long to poll the status register before giving up. PS/2 controller
/// responses normally arrive within a few milliseconds.
pub const default_wait_timeout_nanoseconds: u64 = 10_000_000; // 10 ms

/// A PS/2 device reset (0xFF) runs the device's self-test (BAT), whose reply can
/// take far longer than an ordinary controller response.
pub const device_reset_timeout_nanoseconds: u64 = 750_000_000; // 750 ms

/// PS/2 controller commands, written to the command register (port 0x64).
pub const cmd_read_configuration_byte: u8 = 0x20; // read controller configuration byte (internal RAM byte 0)
pub const cmd_write_configuration_byte: u8 = 0x60; // write controller configuration byte (internal RAM byte 0)
pub const cmd_disable_second_port: u8 = 0xA7; // disable second PS/2 port (dual-channel controllers only)
pub const cmd_enable_second_port: u8 = 0xA8; // enable second PS/2 port (dual-channel controllers only)
pub const cmd_test_second_port: u8 = 0xA9; // test second PS/2 port
pub const cmd_test_controller: u8 = 0xAA; // controller self-test
pub const cmd_test_first_port: u8 = 0xAB; // test first PS/2 port
pub const cmd_diagnostic_dump: u8 = 0xAC; // read all bytes of internal RAM
pub const cmd_disable_first_port: u8 = 0xAD; // disable first PS/2 port
pub const cmd_enable_first_port: u8 = 0xAE; // enable first PS/2 port
pub const cmd_read_controller_input_port: u8 = 0xC0; // read controller input port
pub const cmd_read_controller_output_port: u8 = 0xD0; // read controller output port
pub const cmd_write_controller_output_port: u8 = 0xD1; // write next data byte to the controller output port
pub const cmd_write_first_port_output: u8 = 0xD2; // write next data byte to the first port output buffer
pub const cmd_write_second_port_output: u8 = 0xD3; // write next data byte to the second port output buffer
pub const cmd_write_second_port_input: u8 = 0xD4; // write next data byte to the second port input buffer (to the mouse)
pub const cmd_pulse_system_reset: u8 = 0xFE; // pulse output line 0 low: resets the CPU

/// PS/2 status register bits (read from the status port, 0x64). Bit 4 is
/// chipset-specific and intentionally omitted.
pub const status_output_buffer_full: u8 = 1 << 0; // 1 = a byte is waiting to be read from the data port
pub const status_input_buffer_full: u8 = 1 << 1; // 1 = the controller has not yet consumed the last write
pub const status_system_flag: u8 = 1 << 2; // set once the controller passes POST
pub const status_command_or_data: u8 = 1 << 3; // 1 = last write was a command, 0 = data
/// Chipset-specific in the original spec, universal in practice on dual-channel
/// controllers: set = the waiting byte came from the second port (the mouse).
pub const status_auxiliary_output: u8 = 1 << 5;
pub const status_timeout_error: u8 = 1 << 6; // 1 = time-out error
pub const status_parity_error: u8 = 1 << 7; // 1 = parity error

/// Controller configuration byte bits (internal RAM byte 0; read/written via 0x20/0x60).
pub const configuration_first_port_interrupt: u8 = 1 << 0; // 1 = first port IRQ (IRQ1) enabled
pub const configuration_second_port_interrupt: u8 = 1 << 1; // 1 = second port IRQ (IRQ12) enabled
pub const configuration_system_flag: u8 = 1 << 2; // 1 = system passed POST
pub const configuration_first_port_clock_disabled: u8 = 1 << 4; // 1 = first port clock disabled
pub const configuration_second_port_clock_disabled: u8 = 1 << 5; // 1 = second port clock disabled
pub const configuration_first_port_translation: u8 = 1 << 6; // 1 = first port scancode translation enabled

/// Controller output port bits (read/written via 0xD0/0xD1).
pub const output_port_system_reset: u8 = 1 << 0; // WARNING: keep this 1; writing 0 can lock the machine
pub const output_port_a20_gate: u8 = 1 << 1; // A20 gate
pub const output_port_second_port_clock: u8 = 1 << 2; // dual-channel controllers only
pub const output_port_second_port_data: u8 = 1 << 3; // dual-channel controllers only
pub const output_port_first_port_output_full: u8 = 1 << 4; // output buffer full from first port (IRQ1)
pub const output_port_second_port_output_full: u8 = 1 << 5; // output buffer full from second port (IRQ12)
pub const output_port_first_port_clock: u8 = 1 << 6; // first port clock
pub const output_port_first_port_data: u8 = 1 << 7; // first port data

/// Controller self-test (0xAA) result codes.
pub const response_controller_test_passed: u8 = 0x55;
pub const response_controller_test_failed: u8 = 0xFC;

/// Port test (0xAB / 0xA9) result codes.
pub const response_port_test_passed: u8 = 0x00;
pub const response_port_test_clock_stuck_low: u8 = 0x01;
pub const response_port_test_clock_stuck_high: u8 = 0x02;
pub const response_port_test_data_stuck_low: u8 = 0x03;
pub const response_port_test_data_stuck_high: u8 = 0x04;

/// PS/2 device commands, written to the data port (0x60) to reach the attached device.
pub const device_cmd_identify: u8 = 0xF2; // identify device
pub const device_cmd_enable_scanning: u8 = 0xF4;
pub const device_cmd_disable_scanning: u8 = 0xF5;
pub const device_cmd_reset: u8 = 0xFF; // reset and run the device self-test (BAT)

/// PS/2 device response bytes, read from the data port (0x60).
pub const device_response_self_test_passed: u8 = 0xAA; // BAT succeeded after a reset
pub const device_response_echo: u8 = 0xEE;
pub const device_response_acknowledge: u8 = 0xFA; // ACK
pub const device_response_self_test_failed_1: u8 = 0xFC; // BAT failure
pub const device_response_self_test_failed_2: u8 = 0xFD; // BAT failure
pub const device_response_resend: u8 = 0xFE; // ask the host to resend the last byte

/// PS/2 device identify (0xF2) reply bytes. A keyboard returns a two-byte id
/// beginning with 0xAB; a mouse returns a single-byte id (0x00/0x03/0x04); an
/// ancient AT keyboard returns nothing at all.
pub const identify_keyboard_mf2: u8 = 0xAB; // first byte of a MF2 keyboard id (a subtype byte follows)
pub const identify_mouse_standard: u8 = 0x00;
pub const identify_mouse_scroll: u8 = 0x03; // mouse with scroll wheel
pub const identify_mouse_five_button: u8 = 0x04; // 5-button mouse

fn waitReadable(id: u64, cmd_index: u64, wait_timeout_nanoseconds: u64) bool {
    const deadline = system.clock() + wait_timeout_nanoseconds;
    while (system.clock() < deadline) {
        if (status(id, cmd_index) & status_output_buffer_full != 0) return true; // OBF set -> data ready
    }
    return false;
}

fn waitWritable(id: u64, cmd_index: u64, wait_timeout_nanoseconds: u64) bool {
    const deadline = system.clock() + wait_timeout_nanoseconds;
    while (system.clock() < deadline) {
        if (status(id, cmd_index) & status_input_buffer_full == 0) return true; // IBF clear -> ok to write
    }
    return false; // timed out
}

pub fn status(id: u64, cmd_index: u64) u8 {
    return @intCast(device.ioRead(id, cmd_index, 0, 1) orelse 0);
}

pub fn sendCommand(id: u64, cmd_index: u64, byte: u8, timeout_nanoseconds: u64) bool {
    // wait IBF clear
    if (!waitWritable(id, cmd_index, timeout_nanoseconds)) return false;
    return device.ioWrite(id, cmd_index, 0, 1, byte);
}

pub fn readData(id: u64, status_index: u64, data_index: u64, timeout_nanoseconds: u64) ?u8 {
    // OBF lives in the status register (0x64); wait for it there, then read the data port (0x60)
    if (!waitReadable(id, status_index, timeout_nanoseconds)) return null;
    return @intCast(device.ioRead(id, data_index, 0, 1) orelse 0);
}

pub fn writeData(id: u64, status_index: u64, data_index: u64, byte: u8, timeout_nanoseconds: u64) bool {
    // IBF lives in the status register (0x64); wait for it to clear there, then write the data port
    // (0x60)
    if (!waitWritable(id, status_index, timeout_nanoseconds)) return false;
    return device.ioWrite(id, data_index, 0, 1, byte);
}

pub const Port = enum(u2) {
    one,
    two,

    /// Command register byte that disables this port.
    fn disableCommand(self: Port) u8 {
        return switch (self) {
            .one => cmd_disable_first_port,
            .two => cmd_disable_second_port,
        };
    }

    /// Command register byte that enables this port (and its clock).
    fn enableCommand(self: Port) u8 {
        return switch (self) {
            .one => cmd_enable_first_port,
            .two => cmd_enable_second_port,
        };
    }

    /// Command register byte that runs this port's interface test.
    fn testCommand(self: Port) u8 {
        return switch (self) {
            .one => cmd_test_first_port,
            .two => cmd_test_second_port,
        };
    }

    /// Configuration-byte bit that, when set, disables this port's clock.
    pub fn clockDisabledBit(self: Port) u8 {
        return switch (self) {
            .one => configuration_first_port_clock_disabled,
            .two => configuration_second_port_clock_disabled,
        };
    }

    /// Configuration-byte bit that, when set, enables this port's interrupt.
    pub fn interruptBit(self: Port) u8 {
        return switch (self) {
            .one => configuration_first_port_interrupt,
            .two => configuration_second_port_interrupt,
        };
    }

    /// Command register byte that writes the next data byte into this port's
    /// output buffer (makes a byte appear as if it came from the device).
    pub fn writeOutputBufferCommand(self: Port) u8 {
        return switch (self) {
            .one => cmd_write_first_port_output,
            .two => cmd_write_second_port_output,
        };
    }

    /// Controller command that must prefix a byte destined for this port's
    /// device. Port 1 is the default target of the data port, so it needs no
    /// prefix (null); port 2 requires the "write second port input" command.
    pub fn deviceInputCommand(self: Port) ?u8 {
        return switch (self) {
            .one => null,
            .two => cmd_write_second_port_input,
        };
    }

    /// Controller output-port bit driving this port's clock line.
    pub fn outputPortClockBit(self: Port) u8 {
        return switch (self) {
            .one => output_port_first_port_clock,
            .two => output_port_second_port_clock,
        };
    }

    /// Controller output-port bit driving this port's data line.
    pub fn outputPortDataBit(self: Port) u8 {
        return switch (self) {
            .one => output_port_first_port_data,
            .two => output_port_second_port_data,
        };
    }

    /// Controller output-port bit set when this port's output buffer is full
    /// (wired to the port's IRQ line).
    pub fn outputPortBufferFullBit(self: Port) u8 {
        return switch (self) {
            .one => output_port_first_port_output_full,
            .two => output_port_second_port_output_full,
        };
    }
};

/// The kind of device attached to a port, as reported by the device itself in
/// response to the identify command — not assumed from the port number. Fixed
/// `u32` values because the type also travels in an `AttachRequest`.
pub const DeviceType = enum(u32) {
    keyboard = 0,
    mouse = 1,
    unknown = 2,

    /// Initial-ramdisk name of the driver that serves this device type, or null
    /// if we could not classify it.
    pub fn driverName(self: DeviceType) ?[]const u8 {
        return switch (self) {
            .keyboard => "ps2-keyboard",
            .mouse => "ps2-mouse",
            .unknown => null,
        };
    }

    /// Canonical ACPI HID for this device type, handed to the spawned driver as
    /// its command-line argument, or null if we could not classify it.
    pub fn hid(self: DeviceType) ?[]const u8 {
        return switch (self) {
            .keyboard => acpi_ids.HardwareId.ps2_keyboard.hid(),
            .mouse => acpi_ids.HardwareId.ps2_mouse.hid(),
            .unknown => null,
        };
    }
};

/// The `_HID`s a PS/2 pointing device (the controller's aux channel) can enumerate
/// under. It is the same 8042 mouse channel whichever id the firmware chose:
/// QEMU/OVMF report the generic `.ps2_mouse` (PNP0F13), VirtualBox reports
/// `.microsoft_ps2_mouse` (PNP0F03). Both mean "the mouse on port two".
pub const mouse_hardware_ids = [_]acpi_ids.HardwareId{ .ps2_mouse, .microsoft_ps2_mouse };

/// Find the aux (mouse) device's ACPI node, whichever of the PS/2-mouse `_HID`s the
/// firmware used — the bus needs it to bind IRQ12, and the mouse driver to confirm
/// its device is present. Returns the first match, or null if none is reported.
pub fn findMouseDescriptor(buffer: []device.DeviceDescriptor) ?device.DeviceDescriptor {
    for (mouse_hardware_ids) |id| {
        if (device.findDeviceDescriptorByHid(buffer, id.hid())) |descriptor| return descriptor;
    }
    return null;
}

// --- the bus <-> child-driver forwarding protocol -----------------------------
//
// The 8042's ports and IRQ1 live on the PNP0303 node that only the ps2-bus driver
// claims, so the child device drivers (ps2-keyboard, ps2-mouse) cannot read port
// 0x60 themselves. Instead each child **attaches**: it calls the bus's well-known
// `ps2_bus` endpoint with an `AttachRequest`, handing over its own endpoint as the
// call's capability. From then on the bus forwards every byte the device sends as
// a `ForwardedByte` via the asynchronous `ipc.send` — the IRQ path in the bus can
// never block on a slow child, and the child never touches the controller.

/// A child driver registering for its device's bytes. `device_type` is a
/// `DeviceType` value; the child's receive endpoint travels as the call's
/// capability (`send_cap`).
pub const AttachRequest = extern struct {
    device_type: u32,
};

/// How the bus answered an `AttachRequest` (`AttachReply.status`).
pub const AttachStatus = enum(i32) {
    ok = 0,
    /// The request was malformed (too short to be an `AttachRequest`).
    invalid_request = -1,
    /// The call carried no endpoint capability to forward to.
    missing_endpoint = -2,
    /// No port identified a device of the requested type.
    no_such_device = -3,
};

/// Reply to an `AttachRequest`. `status` is an `AttachStatus` value.
pub const AttachReply = extern struct {
    status: i32,
    _padding: u32 = 0,
};

/// One raw byte read from the data port, forwarded to the attached child whose
/// port it came from (routed by the status register's auxiliary-output bit).
pub const ForwardedByte = extern struct {
    /// The `Port` the byte came from, as `@intFromEnum`.
    port: u32,
    byte: u32,
};

/// A single PS/2 (8042) controller. Construct one with `Controller.init` and
/// drive the controller through its methods; there is only ever one 8042 per
/// machine, but holding the resolved resource indices in an instance keeps the
/// call sites free of global state.
pub const Controller = struct {
    device_id: u64,
    /// Resource index of the command/status port (0x64).
    status_index: u64,
    /// Resource index of the data port (0x60).
    data_index: u64,

    /// Resolve the controller's IO-port resource indices from its device
    /// descriptor. Returns null if either the data or command/status port is
    /// missing from the descriptor.
    pub fn init(device_descriptor: device.DeviceDescriptor) ?Controller {
        var data_index: ?u64 = null;
        var status_index: ?u64 = null;

        for (device_descriptor.resources, 0..device_descriptor.resource_count) |resource, resource_index| {
            if (resource.kind != @intFromEnum(device.ResourceKind.io_port)) continue;
            if (resource.start == dataPort) {
                data_index = @intCast(resource_index);
            } else if (resource.start == statusRegisterPort) {
                status_index = @intCast(resource_index);
            }
        }

        return .{
            .device_id = device_descriptor.id,
            .data_index = data_index orelse return null,
            .status_index = status_index orelse return null,
        };
    }

    pub fn disablePort(self: Controller, port: Port) void {
        // port enable/disable are controller commands and go to the command register (0x64)
        _ = sendCommand(self.device_id, self.status_index, port.disableCommand(), default_wait_timeout_nanoseconds);
    }

    pub fn enablePort(self: Controller, port: Port) void {
        // enabling a port also starts its clock
        _ = sendCommand(self.device_id, self.status_index, port.enableCommand(), default_wait_timeout_nanoseconds);
    }

    /// Run a port's interface test. Returns the controller's reply — compare it
    /// to `response_port_test_passed` (0x00) — or null on timeout.
    pub fn testPort(self: Controller, port: Port) ?u8 {
        if (!sendCommand(self.device_id, self.status_index, port.testCommand(), default_wait_timeout_nanoseconds)) return null;
        return readData(self.device_id, self.status_index, self.data_index, default_wait_timeout_nanoseconds);
    }

    pub fn flushOutputBuffer(self: Controller) void {
        // flush any stale byte the controller buffered
        _ = device.ioRead(self.device_id, self.data_index, 0, 1);
    }

    pub fn readConfigurationByte(self: Controller) ?u8 {
        // ask the controller to place its configuration byte in the output buffer, then read it
        if (!sendCommand(self.device_id, self.status_index, cmd_read_configuration_byte, default_wait_timeout_nanoseconds)) return null;
        return readData(self.device_id, self.status_index, self.data_index, default_wait_timeout_nanoseconds);
    }

    pub fn writeConfigurationByte(self: Controller, update_byte: u8) ?u8 {
        // command 0x60 makes the controller store the next data-port byte as its configuration byte
        if (!sendCommand(self.device_id, self.status_index, cmd_write_configuration_byte, default_wait_timeout_nanoseconds)) return null;
        if (!writeData(self.device_id, self.status_index, self.data_index, update_byte, default_wait_timeout_nanoseconds)) return null;
        return update_byte;
    }

    /// Run the controller self-test. Returns the reply — compare it to
    /// `response_controller_test_passed` (0x55) — or null on timeout.
    pub fn performSelfTest(self: Controller) ?u8 {
        if (!sendCommand(self.device_id, self.status_index, cmd_test_controller, default_wait_timeout_nanoseconds)) return null;
        return readData(self.device_id, self.status_index, self.data_index, default_wait_timeout_nanoseconds);
    }

    /// Detect whether this is a dual-channel controller by temporarily enabling
    /// port 2 and checking whether its clock turned on. Note: this leaves port 2
    /// enabled; the caller should disable it again to keep the bus quiet until
    /// device bring-up.
    pub fn hasTwoChannels(self: Controller) ?bool {
        self.enablePort(.two);
        const configuration = self.readConfigurationByte() orelse return null;
        return (configuration & Port.two.clockDisabledBit()) == 0;
    }

    /// Reset the device attached to `port` (device command 0xFF) and wait for
    /// its power-on self-test (BAT) result. Returns true if the device both
    /// acknowledged and passed, false if it reported a self-test failure, or
    /// null on timeout. The BAT reply can be slow, so the response reads use
    /// `device_reset_timeout_nanoseconds`.
    pub fn resetDevice(self: Controller, port: Port) ?bool {
        // A byte destined for port 2 must be prefixed with the "write to second
        // port input buffer" controller command (0xD4); port 1 is the default.
        if (port.deviceInputCommand()) |prefix| {
            if (!sendCommand(self.device_id, self.status_index, prefix, default_wait_timeout_nanoseconds)) return null;
        }
        if (!writeData(self.device_id, self.status_index, self.data_index, device_cmd_reset, default_wait_timeout_nanoseconds)) return null;

        // A successful reset yields both an ACK (0xFA) and a self-test-passed
        // byte (0xAA). Their order is not guaranteed, so accept either ordering.
        var saw_acknowledge = false;
        var saw_self_test_passed = false;
        var reads: u8 = 0;
        while (reads < 2) : (reads += 1) {
            const reply = readData(self.device_id, self.status_index, self.data_index, device_reset_timeout_nanoseconds) orelse return null;
            switch (reply) {
                device_response_acknowledge => saw_acknowledge = true,
                device_response_self_test_passed => saw_self_test_passed = true,
                device_response_self_test_failed_1, device_response_self_test_failed_2 => return false,
                else => {},
            }
        }
        return saw_acknowledge and saw_self_test_passed;
    }

    /// Send one command byte to the device on `port` (applying the port-2 prefix
    /// as needed) and consume its acknowledgement. Returns true on ACK (0xFA),
    /// false on any other reply, or null on timeout.
    pub fn sendToDevice(self: Controller, port: Port, byte: u8) ?bool {
        if (port.deviceInputCommand()) |prefix| {
            if (!sendCommand(self.device_id, self.status_index, prefix, default_wait_timeout_nanoseconds)) return null;
        }
        if (!writeData(self.device_id, self.status_index, self.data_index, byte, default_wait_timeout_nanoseconds)) return null;
        const reply = readData(self.device_id, self.status_index, self.data_index, default_wait_timeout_nanoseconds) orelse return null;
        return reply == device_response_acknowledge;
    }

    /// Discard any bytes sitting in the output buffer (for example the device-id
    /// byte a mouse emits after a reset) so they cannot be mistaken for the reply
    /// to a subsequent command.
    pub fn drainOutputBuffer(self: Controller) void {
        var guard: u8 = 0;
        while (guard < 16) : (guard += 1) {
            if (status(self.device_id, self.status_index) & status_output_buffer_full == 0) return;
            _ = device.ioRead(self.device_id, self.data_index, 0, 1);
        }
    }

    /// Ask the device on `port` what it is (command 0xF2) and classify the reply.
    /// Scanning is disabled around the query so a streaming device cannot inject
    /// data bytes that look like the identifier. Returns the device type, or null
    /// if the identify command itself timed out.
    pub fn identifyDevice(self: Controller, port: Port) ?DeviceType {
        // Clear any leftover bytes (e.g. a post-reset mouse id) before we start.
        self.drainOutputBuffer();

        // Stop the device reporting so its data can't be mistaken for the reply.
        if (self.sendToDevice(port, device_cmd_disable_scanning) == null) return null;

        if (self.sendToDevice(port, device_cmd_identify) == null) return null;

        // After the ACK, the device sends 0, 1, or 2 identifier bytes.
        const first = readData(self.device_id, self.status_index, self.data_index, default_wait_timeout_nanoseconds);
        const device_type: DeviceType = if (first) |id| switch (id) {
            identify_keyboard_mf2 => blk: {
                // A MF2 keyboard sends a second subtype byte; consume and ignore it.
                _ = readData(self.device_id, self.status_index, self.data_index, default_wait_timeout_nanoseconds);
                break :blk .keyboard;
            },
            identify_mouse_standard, identify_mouse_scroll, identify_mouse_five_button => .mouse,
            else => .unknown,
        } else
            // No identifier bytes at all is a legacy AT keyboard.
            .keyboard;

        // Resume scanning so the device works once its driver takes over.
        _ = self.sendToDevice(port, device_cmd_enable_scanning);
        return device_type;
    }
};
