/*
 * hal_apic.c - Advanced Programmable Interrupt Controller
 *
 * Copyright (c) 2017 Maxime Villard
 *
 * This file is part of ALMOS-MKH.
 *
 * ALMOS-MKH is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License as published by
 * the Free Software Foundation; version 2.0 of the License.
 *
 * ALMOS-MKH 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
 * General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with ALMOS-MKH; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
 */

#include <hal_kernel_types.h>
#include <hal_boot.h>
#include <hal_register.h>
#include <hal_segmentation.h>
#include <hal_apic.h>
#include <hal_internal.h>

#include <memcpy.h>
#include <thread.h>
#include <string.h>
#include <process.h>
#include <printk.h>
#include <vmm.h>
#include <core.h>
#include <cluster.h>

/* -------------------------------------------------------------------------- */

#define PIC1_CMD	0x0020
#define PIC1_DATA	0x0021
#define PIC2_CMD	0x00a0
#define PIC2_DATA	0x00a1

static void hal_pic_init()
{
	/*
	 * Disable the PIC (8259A). We are going to use IOAPIC instead.
	 */
	out8(PIC1_DATA, 0xFF);
	out8(PIC2_DATA, 0xFF);
}

/* -------------------------------------------------------------------------- */

static uint64_t pit_ticks_base __in_kdata = 0;
static uint16_t pit_ticks_last __in_kdata = 0;

#define PIT_FREQUENCY	1193182
#define HZ		100 /* 1/HZ = 10ms */
#define muHZ		1000000 /* 1/muHZ = 1 microsecond */

#define PIT_TIMER0	0x40

#define PIT_CMD		0x43
#	define CMD_BINARY	0x00 /* Use Binary counter values */
#	define CMD_BCD		0x01 /* Use Binary Coded Decimal counter values */
#	define CMD_MODE0	0x00 /* Interrupt on Terminal Count */
#	define CMD_MODE1	0x02 /* Hardware Retriggerable One-Shot */
#	define CMD_MODE2	0x04 /* Rate Generator */
#	define CMD_MODE3	0x06 /* Square Wave */
#	define CMD_MODE4	0x08 /* Software Triggered Strobe */
#	define CMD_MODE5	0x0a /* Hardware Triggered Strobe */
#	define CMD_LATCH	0x00 /* latch counter for reading */
#	define CMD_LSB		0x10 /* LSB, 8 bits */
#	define CMD_MSB		0x20 /* MSB, 8 bits */
#	define CMD_16BIT	0x30 /* LSB and MSB, 16 bits */
#	define CMD_COUNTER0	0x00
#	define CMD_COUNTER1	0x40
#	define CMD_COUNTER2	0x80
#	define CMD_READBACK	0xc0

void hal_pit_reset(uint16_t val)
{
	uint8_t lo, hi;

	pit_ticks_base = 0;

	/* Set the initial counter value for clock 0. */
	out8(PIT_CMD, CMD_COUNTER0|CMD_MODE2|CMD_16BIT);
	out8(PIT_TIMER0, (val >> 0) & 0xFF);
	out8(PIT_TIMER0, (val >> 8) & 0xFF);

	/*
	 * Read the current value, to initialize pit_ticks_last. Not extremely
	 * accurate, but fine.
	 */
	out8(PIT_CMD, CMD_COUNTER0|CMD_LATCH);
	lo = in8(PIT_TIMER0);
	hi = in8(PIT_TIMER0);

	pit_ticks_last = (hi << 8) | lo;
}

uint64_t
hal_pit_timer_read()
{
	uint8_t lo, hi;
	uint16_t ctr;
	uint64_t ticks;

	/* Read the current timer counter. */
	out8(PIT_CMD, CMD_COUNTER0|CMD_LATCH);
	lo = in8(PIT_TIMER0);
	hi = in8(PIT_TIMER0);
	ctr = (hi << 8) | lo;

	/* If the counter has overflown, assume we're into the next tick. */
	if (ctr > pit_ticks_last)
		pit_ticks_base += 0xFFFF;
	pit_ticks_last = ctr;

	ticks = pit_ticks_base + (0xFFFF - ctr);

	return ticks;
}

/*
 * Wait approximately n microseconds.
 */
void
hal_pit_delay(uint64_t n)
{
	uint64_t nticks;

	nticks = 0;
	hal_pit_reset(0xFFFF);

	while (n > 0) {
		while (hal_pit_timer_read() - nticks < (PIT_FREQUENCY + muHZ/2) / muHZ) {
			/* Wait 1/muHZ sec = 1 microsecond */
		}
		nticks++; // ???
		n--;
	}
}

/* -------------------------------------------------------------------------- */

#define BAUDRATE	19200
#define BAUDRATE_DIV	(115200 / BAUDRATE)

#define RS232_COM1_BASE	0x3F8

#define RS232_DATA	0x00
#define RS232_IER	0x01
#	define IER_RD		0x01
#	define IER_TBE		0x02
#	define IER_ER_BRK	0x04
#	define IER_RS232IN	0x08
#define RS232_DIVLO	0x00	/* when DLAB = 1 */
#define RS232_DIVHI	0x01	/* when DLAB = 1 */
#define RS232_IIR	0x02
#define RS232_LCR	0x03
#	define LCR_DATA5	0x00
#	define LCR_DATA6	0x01
#	define LCR_DATA7	0x02
#	define LCR_DATA8	0x03
#	define LCR_TWOSTOP	0x04
#	define LCR_PARITY	0x08
#	define LCR_EVEN		0x10
#	define LCR_STICK	0x20
#	define LCR_DLAB		0x80
#define RS232_MCR	0x04
#	define MCR_DTR		0x01
#	define MCR_RTS		0x02
#	define MCR_ELL		0x04
#	define MCR_IR		0x40
#define RS232_LSR	0x05
#	define LSR_DR		0x01
#	define LSR_OVR		0x02
#	define LSR_PE		0x04
#	define LSR_FE		0x08
#	define LSR_BRK		0x10
#	define LSR_TBE		0x20
#	define LSR_TE		0x40
#define RS232_MSR	0x06
#	define MSR_DCTS		0x01
#	define MSR_DDSR		0x02
#	define MSR_DRI		0x04
#	define MSR_DDCD		0x08
#	define MSR_CTS		0x10
#	define MSR_DSR		0x20
#	define MSR_RI		0x40
#	define MSR_DCD		0x80

#define RS232_SCRATCH	0x07

static bool_t hal_com_received()
{
	return (in8(RS232_COM1_BASE + RS232_LSR) & LSR_DR) != 0;
}

static bool_t hal_com_transmit_empty()
{
	return (in8(RS232_COM1_BASE + RS232_LSR) & LSR_TBE) != 0;
}

char hal_com_read()
{
	while (!hal_com_received());
	return in8(RS232_COM1_BASE + RS232_DATA);
}

void hal_com_send(uint8_t chan, char c)
{
	uint8_t mcr = in8(RS232_COM1_BASE + RS232_MCR);
	out8(RS232_COM1_BASE + RS232_MCR, mcr | MCR_RTS);

	while (!hal_com_transmit_empty());
	out8(RS232_COM1_BASE + RS232_DATA, chan | 0x80);
	out8(RS232_COM1_BASE + RS232_DATA, c);

	out8(RS232_COM1_BASE + RS232_MCR, mcr);
}

/*
 * Called early to provide x86-specific messages. Interrupts disabled.
 */
void hal_com_init_early()
{
	/* Disable all interrupts */
	out8(RS232_COM1_BASE + RS232_IER, 0x00);

	/* Set baudrate */
	out8(RS232_COM1_BASE + RS232_LCR, LCR_DLAB);
	out8(RS232_COM1_BASE + RS232_DIVLO, BAUDRATE_DIV);
	out8(RS232_COM1_BASE + RS232_DIVHI, 0);

	/* 8bits, no parity, one stop bit */
	out8(RS232_COM1_BASE + RS232_LCR, LCR_DATA8);

	/* DTR set, and also DSR */
	out8(RS232_COM1_BASE + RS232_MCR, MCR_DTR|MCR_IR);
	out8(RS232_COM1_BASE + RS232_MSR, MSR_DSR);
}

static void hal_com_init()
{
	/* Disable all interrupts */
	out8(RS232_COM1_BASE + RS232_IER, 0x00);

	/* Set baudrate */
	out8(RS232_COM1_BASE + RS232_LCR, LCR_DLAB);
	out8(RS232_COM1_BASE + RS232_DIVLO, BAUDRATE_DIV);
	out8(RS232_COM1_BASE + RS232_DIVHI, 0);

	/* 8bits, no parity, one stop bit */
	out8(RS232_COM1_BASE + RS232_LCR, LCR_DATA8);

	/* Enable IRQs, DTR set, and also DSR */
	out8(RS232_COM1_BASE + RS232_IER, IER_RD|IER_RS232IN);
	out8(RS232_COM1_BASE + RS232_MCR, MCR_DTR|MCR_IR);
	out8(RS232_COM1_BASE + RS232_MSR, MSR_DSR);
}

/* -------------------------------------------------------------------------- */

size_t ioapic_pins __in_kdata = 0;
paddr_t ioapic_pa __in_kdata = 0;
vaddr_t ioapic_va __in_kdata = 0;

#define IOREGSEL	0x00
#define IOWIN	0x10

#define IOAPICID	0x00
#define IOAPICVER	0x01
#define IOAPICARB	0x02

#define IOREDTBL	0x10
#	define IOREDTBL_DEL_FIXED	0x000
#	define IOREDTBL_DEL_LOPRI	0x100
#	define IOREDTBL_DEL_SMI		0x200
#	define IOREDTBL_DEL_NMI		0x400
#	define IOREDTBL_DEL_INIT	0x500
#	define IOREDTBL_DEL_EXTINT	0x700
#	define IOREDTBL_DEM_PHYS	0x000
#	define IOREDTBL_DEM_LOGIC	0x800
#	define IOREDTBL_DES_SHIFT	56
#	define IOREDTBL_MSK		0x10000

void hal_ioapic_write(uint8_t reg, uint32_t val)
{
	*((volatile uint32_t *)((uint8_t *)ioapic_va + IOREGSEL)) = reg;
	*((volatile uint32_t *)((uint8_t *)ioapic_va + IOWIN)) = val;
}

uint32_t hal_ioapic_read(uint8_t reg)
{
	*((volatile uint32_t *)((uint8_t *)ioapic_va + IOREGSEL)) = reg;
	return *((volatile uint32_t *)((uint8_t *)ioapic_va + IOWIN));
}

void hal_ioapic_bind_irq(uint8_t irq, uint8_t vec, uint8_t dest)
{
	const uint64_t data = ((uint64_t)dest << IOREDTBL_DES_SHIFT) |
	    IOREDTBL_DEM_PHYS | IOREDTBL_DEL_FIXED | IOREDTBL_MSK | vec;

	hal_ioapic_write(IOREDTBL + irq * 2, (uint32_t)(data & 0xFFFFFFFF));
	hal_ioapic_write(IOREDTBL + irq * 2 + 1, (uint32_t)(data >> 32));
}

void hal_ioapic_enable_irq(uint8_t irq)
{
	uint32_t data[2];

	data[0] = hal_ioapic_read(IOREDTBL + irq * 2);
	data[1] = hal_ioapic_read(IOREDTBL + irq * 2 + 1);

	data[0] &= ~IOREDTBL_MSK;

	hal_ioapic_write(IOREDTBL + irq * 2, data[0]);
	hal_ioapic_write(IOREDTBL + irq * 2 + 1, data[1]);
}

void hal_ioapic_disable_irq(uint8_t irq)
{
	uint32_t data[2];

	data[0] = hal_ioapic_read(IOREDTBL + irq * 2);
	data[1] = hal_ioapic_read(IOREDTBL + irq * 2 + 1);

	data[0] |= IOREDTBL_MSK;

	hal_ioapic_write(IOREDTBL + irq * 2, data[0]);
	hal_ioapic_write(IOREDTBL + irq * 2 + 1, data[1]);
}

static void hal_ioapic_init()
{
	uint32_t ver;
	size_t i;

	ver = hal_ioapic_read(IOAPICVER);
	ioapic_pins = ((ver >> 16) & 0xFF) + 1;

	/* Explicitly disable (mask) each vector */
	for (i = 0; i < ioapic_pins; i++) {
		hal_ioapic_disable_irq(i);
	}

	x86_printf("IOAPICPINS: #%z\n", ioapic_pins);

	/* Now, enable the com1 port and the keyboard */
	hal_ioapic_bind_irq(IRQ_COM1, IOAPIC_COM1_VECTOR, 0);
	hal_ioapic_enable_irq(IRQ_COM1);
	hal_ioapic_bind_irq(IRQ_KEYBOARD, IOAPIC_KEYBOARD_VECTOR, 0);
	hal_ioapic_enable_irq(IRQ_KEYBOARD);
}

/* -------------------------------------------------------------------------- */

paddr_t lapic_pa __in_kdata = 0;
vaddr_t lapic_va __in_kdata = 0;

void hal_lapic_write(uint32_t reg, uint32_t val)
{
	*((volatile uint32_t *)((uint8_t *)lapic_va + reg)) = val;
}

uint32_t hal_lapic_read(uint32_t reg)
{
	return *((volatile uint32_t *)((uint8_t *)lapic_va + reg));
}

uint32_t hal_lapic_gid()
{
	return hal_lapic_read(LAPIC_ID) >> LAPIC_ID_SHIFT;
}

static void hal_lapic_icr_wait()
{
	while ((hal_lapic_read(LAPIC_ICRLO) & LAPIC_DLSTAT_BUSY) != 0) {
		pause();
	}
}

/*
 * Use the PIT, which has a standard clock frequency, to determine the CPU's
 * exact bus frequency.
 */
static void hal_lapic_calibrate()
{
	uint64_t pittick, lapictick0, lapictick1;
	uint32_t lapicticks, lapicstart;

	/* Initialize the LAPIC timer to the maximum value */
	hal_lapic_write(LAPIC_ICR_TIMER, 0xFFFFFFFF);

	/* Reset the PIT */
	hal_pit_reset(0xFFFF);

	pittick = hal_pit_timer_read() + 1;
	while (hal_pit_timer_read() < pittick) {
		/* Wait until start of a PIT tick */
	}

	/* Read base count from LAPIC */
	lapictick0 = hal_lapic_read(LAPIC_CCR_TIMER);

	while (hal_pit_timer_read() < pittick + (PIT_FREQUENCY + HZ/2) / HZ) {
		/* Wait 1/HZ sec = 10ms */
	}

	/* Read final count from LAPIC */
	lapictick1 = hal_lapic_read(LAPIC_CCR_TIMER);

	if (lapictick1 > lapictick0)
		x86_panic("LAPIC tick overflow!");

	/* Total number of LAPIC ticks per 1/HZ tick */
	lapicticks = (lapictick0 - lapictick1);

	/*
	 * Finally, calibrate the timer, with an interrupt each 1s (10ms * 100).
	 */
	lapicstart = (lapicticks * 100);
	hal_lapic_write(LAPIC_ICR_TIMER, lapicstart);
	x86_printf("-> lapicticks: %z\n", (uint64_t)lapicticks);
}

/*
 * We have 8 interrupt sources:
 *  - Spurious
 *  - APIC Timer (TMR)
 *  - Local Interrupt 0 (LINT0)
 *  - Local Interrupt 1 (LINT1)
 *  - Performance Monitor Counters (PMC)
 *  - Thermal Sensors (THM)
 *  - APIC internal error (ERR)
 *  - Extended (Implementation dependent)
 * Only the Spurious and APIC Timer interrupts are enabled.
 */
void cpu_lapic_init()
{
	if ((rdmsr(MSR_APICBASE) & APICBASE_PHYSADDR) != lapic_pa) {
		x86_panic("APICBASE and ACPI don't match!\n");
	}
	wrmsr(MSR_APICBASE, lapic_pa | APICBASE_EN);

	hal_lapic_write(LAPIC_TPR, 0);
	hal_lapic_write(LAPIC_EOI, 0);
	hal_lapic_write(LAPIC_SVR, LAPIC_SVR_ENABLE|VECTOR_APIC_SPURIOU);

	/* Explicitly disable (mask) each vector */
	hal_lapic_write(LAPIC_LVT_TMR, LAPIC_TMR_M);
	hal_lapic_write(LAPIC_LVT_LINT0, LAPIC_LINT_M);
	hal_lapic_write(LAPIC_LVT_LINT1, LAPIC_LINT_M);
	hal_lapic_write(LAPIC_LVT_PMC, LAPIC_PMC_M);
	hal_lapic_write(LAPIC_LVT_THM, LAPIC_THM_M);
	hal_lapic_write(LAPIC_LVT_ERR, LAPIC_ERR_M);

	/* Now, enable the timer in repeated mode. */
	hal_lapic_write(LAPIC_LVT_TMR, LAPIC_TMR_TM|LAPIC_TMR_M);
	hal_lapic_write(LAPIC_DCR_TIMER, LAPIC_DCRT_DIV1);
	hal_lapic_calibrate();
	hal_lapic_write(LAPIC_LVT_TMR, LAPIC_TMR_TM|LAPIC_TIMER_VECTOR);
}

/* -------------------------------------------------------------------------- */

static void
hal_ipi_init(uint32_t gid)
{
	/* clear the error status */
	hal_lapic_write(LAPIC_ESR, 0);
	(void)hal_lapic_read(LAPIC_ESR);

	/* send the IPI */
	hal_lapic_write(LAPIC_ICRHI, gid << LAPIC_ID_SHIFT);
	hal_lapic_write(LAPIC_ICRLO, LAPIC_DLMODE_INIT | LAPIC_LEVEL_ASSERT);
	hal_lapic_icr_wait();

	/* wait 10ms */
	hal_pit_delay(10000);

	hal_lapic_write(LAPIC_ICRLO,
	    LAPIC_DLMODE_INIT | LAPIC_TRIGGER_LEVEL | LAPIC_LEVEL_DEASSERT);
	hal_lapic_icr_wait();
}

static void
hal_ipi_startup(uint32_t gid, paddr_t pa)
{
	/* clear the error status */
	hal_lapic_write(LAPIC_ESR, 0);
	(void)hal_lapic_read(LAPIC_ESR);

	/* send the IPI */
	hal_lapic_icr_wait();
	hal_lapic_write(LAPIC_ICRHI, gid << LAPIC_ID_SHIFT);
	hal_lapic_write(LAPIC_ICRLO, pa | LAPIC_DLMODE_STARTUP |
	    LAPIC_LEVEL_ASSERT);
	hal_lapic_icr_wait();
}

/* -------------------------------------------------------------------------- */

int
boot_cpuN(uint32_t gid, paddr_t pa)
{
	/*
	 * Bootstrap code must be addressable in real mode and it must be
	 * page-aligned.
	 */
	XASSERT(pa < 0x10000 && pa % PAGE_SIZE == 0);

	/*
	 * Local cache flush, in case the BIOS has left the AP with its cache
	 * disabled. It may not be able to cope with MP coherency.
	 */
	wbinvd();

	hal_ipi_init(gid);
	hal_pit_delay(10000);

	hal_ipi_startup(gid, pa / PAGE_SIZE);
	hal_pit_delay(200);

	hal_ipi_startup(gid, pa / PAGE_SIZE);
	hal_pit_delay(200);

	return 0;
}

/* -------------------------------------------------------------------------- */

void hal_apic_init()
{
	/* Disable the PIC */
	hal_pic_init();

	/* Enable the IOAPIC */
	hal_ioapic_init();

	/* Enable the Serial Port */
	hal_com_init();
}

