#include <stdbool.h>
#include <stdlib.h>
#include <stdio.h>
#include <console.h>
#include <sys/io.h>
#include <x86/cpu.h>
#include <syslinux/config.h>

#include "serial.h"

enum {
    THR = 0,
    RBR = 0,
    DLL = 0,
    DLM = 1,
    IER = 1,
    IIR = 2,
    FCR = 2,
    LCR = 3,
    MCR = 4,
    LSR = 5,
    MSR = 6,
    SCR = 7,
};


int serial_init(struct serial_if *sif, const char *argv[])
{
    const struct syslinux_serial_console_info *sci
	= syslinux_serial_console_info();
    uint16_t port;
    unsigned int divisor;
    uint8_t dll, dlm, lcr;

    if (!argv[0]) {
	if (sci->iobase) {
	    port = sci->iobase;
	} else {
	    printf("No port number specified and not using serial console!\n");
	    return -1;
	}
    } else {
	port = strtoul(argv[0], NULL, 0);
	if (port <= 3) {
	    uint16_t addr = ((uint16_t *)0x400)[port];
	    if (!addr) {
		printf("No serial port address found!\n");
	    return -1;
	    }
	    printf("Serial port %u is at 0x%04x\n", port, addr);
	    port = addr;
	}
    }

    sif->port = port;
    sif->console = false;

    divisor = 1;		/* Default speed = 115200 bps */

    /* Check to see if this is the same as the serial console */
    if (port == sci->iobase) {
	/* Overlaying the console... */
	sif->console = true;

	/* Default to already configured speed */
	divisor = sci->divisor;

	/* Shut down I/O to the console for the time being */
	openconsole(&dev_null_r, &dev_null_w);
    }

    if (argv[0] && argv[1])
	divisor = 115200/strtoul(argv[1], NULL, 0);

    cli();			/* Just in case... */

    /* Save old register settings */
    sif->old.lcr = inb(port + LCR);
    sif->old.mcr = inb(port + MCR);
    sif->old.iir = inb(port + IIR);

    /* Set speed */
    outb(0x83, port + LCR);	/* Enable divisor access */
    sif->old.dll = inb(port + DLL);
    sif->old.dlm = inb(port + DLM);
    outb(divisor, port + DLL);
    outb(divisor >> 8, port + DLM);
    (void)inb(port + IER);	/* Synchronize */

    dll = inb(port + DLL);
    dlm = inb(port + DLM);
    lcr = inb(port + LCR);
    outb(0x03, port + LCR);	/* Enable data access, n81 */
    (void)inb(port + IER);	/* Synchronize */
    sif->old.ier = inb(port + IER);

    /* Disable interrupts */
    outb(0, port + IER);

    sti();

    if (dll != (uint8_t)divisor ||
	dlm != (uint8_t)(divisor >> 8) ||
	lcr != 0x83) {
	serial_cleanup(sif);
	printf("No serial port detected!\n");
	return -1;		/* This doesn't look like a serial port */
    }

    /* Enable 16550A FIFOs if available */
    outb(0x01, port + FCR);	/* Enable FIFO */
    (void)inb(port + IER);	/* Synchronize */
    if (inb(port + IIR) < 0xc0)
	outb(0x00, port + FCR);	/* Disable FIFOs if non-functional */
    (void)inb(port + IER);	/* Synchronize */

    return 0;
}

void serial_write(struct serial_if *sif, const void *data, size_t n)
{
    uint16_t port = sif->port;
    const char *p = data;
    uint8_t lsr;

    while (n--) {
	do {
	    lsr = inb(port + LSR);
	} while (!(lsr & 0x20));

	outb(*p++, port + THR);
    }
}

void serial_read(struct serial_if *sif, void *data, size_t n)
{
    uint16_t port = sif->port;
    char *p = data;
    uint8_t lsr;

    while (n--) {
	do {
	    lsr = inb(port + LSR);
	} while (!(lsr & 0x01));

	*p++ = inb(port + RBR);
    }
}

void serial_cleanup(struct serial_if *sif)
{
    uint16_t port = sif->port;

    outb(0x83, port + LCR);
    (void)inb(port + IER);
    outb(sif->old.dll, port + DLL);
    outb(sif->old.dlm, port + DLM);
    (void)inb(port + IER);
    outb(sif->old.lcr & 0x7f, port + LCR);
    (void)inb(port + IER);
    outb(sif->old.mcr, port + MCR);
    outb(sif->old.ier, port + IER);
    if (sif->old.iir < 0xc0)
	outb(0x00, port + FCR);	/* Disable FIFOs */

    /* Re-enable console messages, if we shut them down */
    if (sif->console)
	openconsole(&dev_null_r, &dev_stdcon_w);
}
