/*
 * Serial I/O Driver
 * main.c
 *
 * Copyright (C) 2015 Aleksandar Andrejevic <theflash@sdf.lonestar.org>
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

#include "serial.h"
#include <irq.h>
#include <memory.h>
#include <thread.h>
#include <heap.h>
#include <stdio.h>

const char driver_name[] = "serial";

static byte_t num_ports = 0;

static dword_t serial_init(void);
static dword_t serial_cleanup(void);
static dword_t serial_read(device_t *device, void *buffer, size_t length, size_t *bytes_read);
static dword_t serial_write(device_t *device, const void *buffer, size_t length, size_t *bytes_read);
static dword_t serial_ioctl(device_t *device, dword_t control_code, const void *in_buffer, size_t in_length, void *out_buffer, size_t out_length);

static char_dev_driver_t serial_driver =
{
    .init_proc = serial_init,
    .cleanup_proc = serial_cleanup,
    .read_proc = serial_read,
    .write_proc = serial_write,
    .ioctl_proc = serial_ioctl
};

static void set_baud_rate(serial_port_t *device, dword_t baud)
{
    word_t divisor = SERIAL_BAUD_FREQ / baud;
    if (divisor == 0) divisor = 1;

    cpu_write_port_byte(device->port + SERIAL_LCR, cpu_read_port_byte(device->port + SERIAL_LCR) | SERIAL_DLAB_BIT);
    cpu_write_port_byte(device->port + SERIAL_DLR, divisor & 0xFF);
    cpu_write_port_byte(device->port + SERIAL_DHR, divisor >> 8);
    cpu_write_port_byte(device->port + SERIAL_LCR, cpu_read_port_byte(device->port + SERIAL_LCR) & ~SERIAL_DLAB_BIT);
}

static void serial_irq_handler(registers_t *regs, byte_t irq_num)
{
    dword_t i;
    char name[MAX_DEVICE_NAME];

    UNUSED_PARAMETER(regs);
    UNUSED_PARAMETER(irq_num);

    for (i = 0; i < num_ports; i++)
    {
        sprintf(name, "COM%d", i + 1);
        serial_port_t *device = CONTAINER_OF(get_char_device(name), serial_port_t, header);
        if (!device) continue;

        byte_t status = cpu_read_port_byte(device->port + SERIAL_IIR);

        if ((status & 0x07) == 2) release_semaphore(&device->write_mutex, 1);
        else if ((status & 0x07) == 4) release_semaphore(&device->read_mutex, 1);
    }
}

static dword_t serial_init(void)
{
    dword_t ret;
    dword_t i;
    word_t bda_data[9];
    char name[MAX_DEVICE_NAME];

    ret = read_physical((void*)0x400, &bda_data, sizeof(bda_data));
    if (ret != ERR_SUCCESS) return ret;

    num_ports = (bda_data[8] >> 9) & 0x07;
    if (num_ports == 0) return ERR_NOTFOUND;

    for (i = 0; i < num_ports; i++)
    {
        serial_port_t *device = (serial_port_t*)malloc(sizeof(serial_port_t));
        if (device == NULL)
        {
            ret = ERR_NOMEMORY;
            break;
        }

        sprintf(device->header.name, "COM%d", i + 1);
        device->header.driver = &serial_driver;
        lock_init(&device->header.lock);
        device->port = bda_data[i];

        init_semaphore(&device->read_mutex, 0, 1);
        init_semaphore(&device->write_mutex, 0, 1);

        set_baud_rate(device, SERIAL_BAUD_FREQ);
        cpu_write_port_byte(device->port + SERIAL_IER, 0x03);
        cpu_write_port_byte(device->port + SERIAL_FCR,
                 SERIAL_FIFO_ENABLED_BIT
                 | SERIAL_RECV_FIFO_FLUSH_BIT
                 | SERIAL_SEND_FIFO_FLUSH_BIT);
        cpu_write_port_byte(device->port + SERIAL_LCR, 0x03);
        cpu_write_port_byte(device->port + SERIAL_MCR, 0x00);

        ret = register_char_device(&device->header);
        if (ret != ERR_SUCCESS) goto cleanup;
    }

    ret = register_irq_handler(SERIAL_ODD_IRQ, serial_irq_handler, TRUE);
    if (ret != ERR_SUCCESS) goto cleanup;

    if (num_ports > 1)
    {
        ret = register_irq_handler(SERIAL_EVEN_IRQ, serial_irq_handler, TRUE);
        if (ret != ERR_SUCCESS) goto cleanup;
    }

cleanup:
    if (ret != ERR_SUCCESS)
    {
        for (i = 0; i < num_ports; i++)
        {
            sprintf(name, "COM%d", i + 1);
            device_t *device = get_char_device(name);

            if (device)
            {
                unregister_char_device(device);
                free(device);
            }
        }

        unregister_irq_handler(SERIAL_ODD_IRQ, serial_irq_handler);
        unregister_irq_handler(SERIAL_EVEN_IRQ, serial_irq_handler);
    }

    return ret;
}

static dword_t serial_cleanup(void)
{
    dword_t i;
    char name[MAX_DEVICE_NAME];

    for (i = 0; i < num_ports; i++)
    {
        sprintf(name, "COM%d", i + 1);
        device_t *device = get_char_device(name);

        if (device)
        {
            unregister_char_device(device);
            free(device);
        }
    }

    unregister_irq_handler(SERIAL_ODD_IRQ, serial_irq_handler);
    unregister_irq_handler(SERIAL_EVEN_IRQ, serial_irq_handler);

    return ERR_SUCCESS;
}

static dword_t serial_read(device_t *device, void *buffer, size_t length, size_t *bytes_read)
{
    dword_t i;
    dword_t ret = ERR_SUCCESS;
    thread_t *current_thread = get_current_thread();
    serial_port_t *serial = CONTAINER_OF(device, serial_port_t, header);

    for (i = 0; i < length; i++)
    {
        if (current_thread->terminating)
        {
            ret = ERR_CANCELED;
            break;
        }

        if (!(cpu_read_port_byte(serial->port + SERIAL_LSR) & SERIAL_RECV_RDY_BIT))
        {
            if (wait_semaphore(&serial->read_mutex, 1, (dword_t)-1) == ERR_CANCELED)
            {
                ret = ERR_CANCELED;
                break;
            }
        }

        ((byte_t*)buffer)[i] = cpu_read_port_byte(serial->port + SERIAL_DATA);
    }

    return ret;
}

static dword_t serial_write(device_t *device, const void *buffer, size_t length, size_t *bytes_read)
{
    dword_t i;
    dword_t ret = ERR_SUCCESS;
    thread_t *current_thread = get_current_thread();
    serial_port_t *serial = CONTAINER_OF(device, serial_port_t, header);

    for (i = 0; i < length; i++)
    {
        if (current_thread->terminating)
        {
            ret = ERR_CANCELED;
            break;
        }

        if (!(cpu_read_port_byte(serial->port + SERIAL_LSR) & SERIAL_SEND_RDY_BIT))
        {
            if (wait_semaphore(&serial->write_mutex, 1, (dword_t)-1) == ERR_CANCELED)
            {
                ret = ERR_CANCELED;
                break;
            }
        }

        cpu_write_port_byte(serial->port + SERIAL_DATA, ((const byte_t*)buffer)[i]);
    }

    return ret;
}

static dword_t serial_ioctl(device_t *_device, dword_t control_code, const void *in_buffer, size_t in_length, void *out_buffer, size_t out_length)
{
    serial_port_t *serial = CONTAINER_OF(_device, serial_port_t, header);

    switch (control_code)
    {
    case IOCTL_CHAR_DEV_CHECK_INPUT:
        if (out_length >= sizeof(size_t))
        {
            *((size_t*)out_buffer) = (cpu_read_port_byte(serial->port + SERIAL_LSR) & SERIAL_RECV_RDY_BIT) ? 1 : 0;
            return ERR_SUCCESS;
        }
        else
        {
            return ERR_SMALLBUF;
        }

    case IOCTL_CHAR_DEV_CHECK_OUTPUT:
        if (out_length >= sizeof(size_t))
        {
            *((size_t*)out_buffer) = (cpu_read_port_byte(serial->port + SERIAL_LSR) & SERIAL_SEND_RDY_BIT) ? 1 : 0;
            return ERR_SUCCESS;
        }
        else
        {
            return ERR_SMALLBUF;
        }

    case IOCTL_SERIAL_SET_BAUD_RATE:
        if (in_length >= sizeof(dword_t))
        {
            dword_t baud_rate = *(const dword_t*)in_buffer;
            if (baud_rate > SERIAL_BAUD_FREQ) return ERR_INVALID;

            set_baud_rate(serial, baud_rate);
            return ERR_SUCCESS;
        }
        else
        {
            return ERR_SMALLBUF;
        }

        break;

    default:
        return ERR_INVALID;
    }
}

dword_t driver_load(const char *parameters)
{
    return register_char_dev_driver(&serial_driver);
}

dword_t driver_unload(void)
{
    return unregister_char_dev_driver(&serial_driver);
}
