/*
 * Copyright (c) 2013-2014 Yuichi Watanabe
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * 1. Redistributions of source code must retain the above copyright notice,
 *    this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright notice,
 *    this list of conditions and the following disclaimer in the documentation
 *    and/or other materials provided with the distribution.
 * 3. Neither the name of the Yuichi Watanabe nor the names of its
 *    contributors may be used to endorse or promote products derived from
 *    this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 */

#include <core/acpi.h>
#include <core/acpi_madt.h>
#include <core/initfunc.h>
#include <core/mm.h>
#include <core/mmio.h>
#include <core/printf.h>
#include <core/spinlock.h>
#include <core/string.h>
#include <core/vm.h>
#include <io/io.h>
#include "ioapic_regs.h"

/* #define IOAPIC_DEBUG */
#ifdef IOAPIC_DEBUG
#define IOAPIC_DBG(...)						\
	do {							\
		printf("IOAPIC: " __VA_ARGS__);			\
	} while (0)
#else
#define IOAPIC_DBG(...)
#endif

#define IOAPIC_MAX_NUM 9

#define IOAPIC_ENABLE_EXTINT

struct ioapic {
	spinlock_t	lock;
	int		redir_num;
	char		*virt_base;
	phys_t		phys_base;
	gsi_t		gsi_base;
	bool		initialized;
};

struct ioapic_access {
	spinlock_t	lock;
	u8		index;
	struct ioapic	*ioapic;
};

static drvdata_hdl_t ioapic_handle;
int ioapic_num = 0;
struct ioapic ioapics[IOAPIC_MAX_NUM];

static u32
ioapic_read32(struct ioapic *dev, u8 index)
{
	u32 val;

	spinlock_lock(&dev->lock);
	write8(dev->virt_base + IOAPIC_INDEX_REG_OFFSET, index);
	val = read32(dev->virt_base + IOAPIC_DATA_REG_OFFSET);
	spinlock_unlock(&dev->lock);

	return val;
}

static void
ioapic_write32(struct ioapic *dev, u8 index, u32 val)
{
	spinlock_lock(&dev->lock);
	write8(dev->virt_base + IOAPIC_INDEX_REG_OFFSET, index);
	write32(dev->virt_base + IOAPIC_DATA_REG_OFFSET, val);
	spinlock_unlock(&dev->lock);
}

vmmerr_t
ioapic_read_redir(gsi_t gsi, u32 *high, u32 *low)
{
	int i;
	struct ioapic *dev;

	for (i = 0; i < ioapic_num; i++) {
		dev = ioapics + i;
		if (gsi >= dev->gsi_base
		    && gsi < dev->gsi_base + dev->redir_num)
		{
			goto found;
		}
	}
	return VMMERR_RANGE;

found:
	*high = ioapic_read32(dev, IOAPIC_REDIR_TBL0_LOW_INDEX
			     + IOAPIC_REDIR_TBL_SIZE * gsi + 1);
	*low = ioapic_read32(dev, IOAPIC_REDIR_TBL0_LOW_INDEX
			     + IOAPIC_REDIR_TBL_SIZE * gsi);

	return VMMERR_SUCCESS;
}

static vmmerr_t
ioapic_init_one(struct ioapic *dev, phys_t phys_base, gsi_t gsi_base)
{
	spinlock_init(&dev->lock);
	dev->virt_base = mapmem(MAPMEM_HPHYS | MAPMEM_WRITE | MAPMEM_UC,
				phys_base, IOAPIC_MEM_SIZE);
	if (dev->virt_base == NULL) {
		printf("Failed to map I/O APIC registers.\n");
		return VMMERR_NOMEM;
	}

	dev->phys_base = phys_base;
	dev->gsi_base = gsi_base;

	dev->redir_num = ((ioapic_read32(dev, IOAPIC_VER_REG_INDEX)
			     & IOAPIC_VER_REG_MRE) >> IOAPIC_VER_REG_SHIFT)
		+ 1;
	IOAPIC_DBG("base 0x%llx redir_num %d\n", phys_base, dev->redir_num);
	return VMMERR_SUCCESS;
}


static void
ioapic_parse_apic_structure(struct apic_header *apic_header)
{
	struct madt_io_apic	*madt_io_apic;

	switch (apic_header->type) {
	case APIC_STRUCT_TYPE_IO_APIC:
		if (ioapic_num == IOAPIC_MAX_NUM) {
			printf("I/O apics are more than 0x%d\n",
			       IOAPIC_MAX_NUM);
			break;
		}
		madt_io_apic = (struct madt_io_apic *)apic_header;
		if (ioapic_init_one(ioapics + ioapic_num,
				    madt_io_apic->io_apic_address,
				    madt_io_apic->global_system_interrupt_base)
		    != VMMERR_SUCCESS) {
			break;
		}
		ioapic_num++;
	}
}

static acpi_parser_t ioapic_parse_madt;
static void
ioapic_parse_madt(void *table, u32 length)
{
	struct acpi_madt	*acpi_madt = (struct acpi_madt *)table;
	struct apic_header	*apic_header;

	for (apic_header = (struct apic_header *)(acpi_madt + 1);
	     (ulong)apic_header < (ulong)acpi_madt + length;
	     apic_header = (struct apic_header *)((ulong)apic_header
						  + apic_header->length)) {
		ioapic_parse_apic_structure(apic_header);
	}
}

static void
ioapic_init(void)
{
	ioapic_handle = vm_alloc_driver_data(sizeof(struct ioapic_access));
	acpi_register_parser(ACPI_MADT_SIGNATURE, ioapic_parse_madt);
}

static int
ioapic_mem_reg_access(void *data, phys_t gphys, bool wr, void *buf,
		       uint len, u32 flags)
{
	struct ioapic_access *access = (struct ioapic_access *)data;
	phys_t offset = gphys - access->ioapic->phys_base;
	int done = 0;
	u32 *buf32 = (u32 *)buf;
	u8 *buf8 = (u8 *)buf;

	spinlock_lock(&access->lock);

	if (wr) {
		switch (offset) {
		case IOAPIC_INDEX_REG_OFFSET:
			access->index = *buf8;
			done = 1;
			break;
		case IOAPIC_DATA_REG_OFFSET:
			if (len != 4) {
				break;
			}
			ioapic_write32(access->ioapic, access->index, *buf32);
			done = 1;
			break;
		default:
			mmio_memcpy(access->ioapic->virt_base + offset, buf, len);
			done = 1;
			break;
		}
	} else {
		switch (offset) {
		case IOAPIC_INDEX_REG_OFFSET:
			memset(buf, 0, len);
			*buf8 = access->index;
			done = 1;
			break;
		case IOAPIC_DATA_REG_OFFSET:
			if (len != 4) {
				break;
			}
			*buf32 = ioapic_read32(access->ioapic, access->index);
			done = 1;
			break;
		default:
			mmio_memcpy(buf, access->ioapic->virt_base + offset, len);
			done = 1;
			break;
		}
	}

	if (! done) {
		mmio_do_nothing(data, gphys, wr, buf, len, flags);
	}

	spinlock_unlock(&access->lock);

	return 1; /* emulated */
}

static void
ioapic_setup (void)
{
	struct ioapic_access	*access;
	int			i;

	access = vm_get_driver_data(ioapic_handle);
	spinlock_init(&access->lock);

	if (vm_get_id() != 0) {
		/*
		 * VMn (n > 0) can't access physical I/O APICs.
		 */
		return;
	}

	for (i = 0; i < ioapic_num; i++) {
		access->ioapic = ioapics + i;
		mmio_register(access->ioapic->phys_base, IOAPIC_MEM_SIZE,
			      ioapic_mem_reg_access, access);
	}
}

static void
ioapic_reset(void)
{
	struct ioapic *dev;
	int i, j;

	if (vm_get_id() != 0) {
		return;
	}

	for (i = 0; i < ioapic_num; i++) {
		dev = ioapics + i;
		IOAPIC_DBG("Mask all 0x%llx.\n", dev->phys_base);
		for (j = 0; j < dev->redir_num; j++) {
			ioapic_write32(dev,
				       IOAPIC_REDIR_TBL0_LOW_INDEX
				       + IOAPIC_REDIR_TBL_SIZE * j,
				       IOAPIC_REDIR_LOW_MASK_BIT);
		}
#ifdef IOAPIC_ENABLE_EXTINT
		if (i != 0) {
			continue;
		}
		printf("Enable extint.\n");
		ioapic_write32(dev,
			       IOAPIC_REDIR_TBL0_LOW_INDEX + 1,
			       0);
		ioapic_write32(dev,
			       IOAPIC_REDIR_TBL0_LOW_INDEX,
			       IOAPIC_REDIR_LOW_DELIVERY_MODE_EXTINT);
#endif
	}

}

INTR_INIT2(ioapic_init);
DRIVER_SETUPVM(ioapic_setup);
INTR_RESET2(ioapic_reset);
