///                MentOS, The Mentoring Operating system project
/// @file mouse.c
/// @brief  Driver for *PS2* Mouses.
/// @copyright (c) 2014-2021 This file is distributed under the MIT License.
/// See LICENSE.md for details.is distributed under the MIT License.
/// @addtogroup mouse
/// @{

#include "drivers/mouse.h"
#include "descriptor_tables/isr.h"
#include "hardware/pic8259.h"
#include "io/port_io.h"

/// The mouse starts sending automatic packets.
#define MOUSE_ENABLE_PACKET 0xF4
/// The mouse stops sending automatic packets.
#define MOUSE_DISABLE_PACKET 0xF5
/// Disables streaming, sets the packet rate to 100 per second, and resolution to 4 pixels per mm.
#define MOUSE_USE_DEFAULT_SETTINGS 0xF6

/// Mouse ISR cycle.
static uint8_t mouse_cycle = 0;
/// Mouse communication data.
static int8_t mouse_bytes[3];
/// Mouse x position.
static int32_t mouse_x = (800 / 2);
/// Mouse y position.
static int32_t mouse_y = (600 / 2);

/// @brief      Mouse wait for a command.
/// @param type 1 for sending - 0 for receiving.
static void __mouse_waitcmd(unsigned char type)
{
    register unsigned int _time_out = 100000;
    if (type == 0) {
        // DATA.
        while (_time_out--) {
            if ((inportb(0x64) & 1) == 1) {
                return;
            }
        }
        return;
    } else {
        while (_time_out--) // SIGNALS
        {
            if ((inportb(0x64) & 2) == 0) {
                return;
            }
        }
        return;
    }
}

/// @brief      Send data to mouse.
/// @param data The data to send.
static void __mouse_write(unsigned char data)
{
    __mouse_waitcmd(1);
    outportb(0x64, 0xD4);
    __mouse_waitcmd(1);
    outportb(0x60, data);
}

/// @brief  Read data from mouse.
/// @return The data received from mouse.
static unsigned char __mouse_read()
{
    __mouse_waitcmd(0);
    return inportb(0x60);
}

/// @brief The interrupt service routine of the mouse.
/// @param f The interrupt stack frame.
static void __mouse_isr(pt_regs *f)
{
    (void)f;
    // Get the input bytes.
    mouse_bytes[mouse_cycle++] = (char)inportb(0x60);
    if (mouse_cycle == 3) {
        // Reset the mouse cycle.
        mouse_cycle = 0;
        // ----------------------------
        // Get the X coordinates.
        // ----------------------------
        if ((mouse_bytes[0] & 0x40) == 0) {
            // Bit number 4 of the first byte (value 0x10) indicates that
            // delta X (the 2nd byte) is a negative number, if it is set.
            if ((mouse_bytes[0] & 0x10) == 0) {
                mouse_x -= mouse_bytes[1];
            } else {
                mouse_x += mouse_bytes[1];
            }
        } else {
            // Overflow.
            mouse_x += mouse_bytes[1] / 2;
        }
        // ----------------------------
        // Get the Y coordinates.
        // ----------------------------
        if ((mouse_bytes[0] & 0x80) == 0) {
            // Bit number 5 of the first byte (value 0x20) indicates that
            // delta Y (the 3rd byte) is a negative number, if it is set.
            if ((mouse_bytes[0] & 0x20) == 0) {
                mouse_y -= mouse_bytes[2];
            } else {
                mouse_y += mouse_bytes[2];
            }
        } else {
            // Overflow.
            mouse_y -= mouse_bytes[2] / 2;
        }
        // ----------------------------
        // Apply cursor constraint (800x600).
        // ----------------------------
        if (mouse_x <= 0) {
            mouse_x = 0;
        } else if (mouse_x >= (800 - 16)) {
            mouse_x = 800 - 16;
        }
        if (mouse_y <= 0) {
            mouse_y = 0;
        } else if (mouse_y >= (600 - 24)) {
            mouse_y = 600 - 24;
        }
        // Print the position.
        // pr_default("\rX: %d | Y: %d\n", mouse_x, mouse_y);

        // Move the cursor.
        // video_set_cursor(mouse_x, mouse_y);

        // Here a problem is detected, if the mouse moves
        // Pressed keys are detected.
        // Detecting keystrokes.
        // Center pressed.
        if ((mouse_bytes[0] & 0x04) == 0) {
            // pr_default(LNG_MOUSE_MID);
        }
        // Right pressed.
        if ((mouse_bytes[0] & 0x02) == 0) {
            // pr_default(LNG_MOUSE_RIGHT);
        }
        // Left pressed.
        if ((mouse_bytes[0] & 0x01) == 0) {
            // pr_default(LNG_MOUSE_LEFT);
        }
    }
    pic8259_send_eoi(IRQ_MOUSE);
}

/// @brief Enable the mouse driver.
static void __mouse_enable()
{
    // Enable the mouse interrupts.
    pic8259_irq_enable(IRQ_MOUSE);
    // Disable the mouse.
    __mouse_write(MOUSE_ENABLE_PACKET);
    // Acknowledge.
    __mouse_read();
}

/// @brief Disable the mouse driver.
static void __mouse_disable()
{
    // Disable the mouse interrupts.
    pic8259_irq_disable(IRQ_MOUSE);
    // Disable the mouse.
    __mouse_write(MOUSE_DISABLE_PACKET);
    // Acknowledge.
    __mouse_read();
}

int mouse_initialize()
{
    // Enable the auxiliary mouse device.
    __mouse_waitcmd(1);
    outportb(0x64, 0xA8);

    // Enable the interrupts.
    __mouse_waitcmd(1);
    outportb(0x64, 0x20);
    __mouse_waitcmd(0);
    uint8_t status_byte = (inportb(0x60) | 2);
    __mouse_waitcmd(1);
    outportb(0x64, 0x60);
    __mouse_waitcmd(1);
    outportb(0x60, status_byte);

    // Tell the mouse to use default settings.
    __mouse_write(MOUSE_USE_DEFAULT_SETTINGS);
    // Acknowledge.
    __mouse_read();

    // Setup the mouse handler.
    irq_install_handler(IRQ_MOUSE, __mouse_isr, "mouse");

    // Enable the mouse.
    __mouse_enable();
    return 0;
}

int mouse_finalize()
{
    // Uninstall the IRQ.
    irq_uninstall_handler(IRQ_MOUSE, __mouse_isr);
    
    // Disable the mouse.
    __mouse_disable();
    return 0;
}

/// @}