/*
 * Copyright (c) 2007, 2008 University of Tsukuba
 * 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 University of Tsukuba 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.
 */
/*
 * Copyright (c) 2010-2014 Yuichi Watanabe
 */

#include <common/common.h>
#include <core/assert.h>
#include <core/cpu.h>
#include <core/initfunc.h>
#include <core/mm.h>
#include <core/mmio.h>
#include <core/spinlock.h>
#include <core/string.h>
#include <io/io.h>
#include "apic.h"
#include "apic_regs.h"
#include "asm.h"
#include "cpu.h"
#include "current.h"
#include "extint.h"
#include "panic.h"
#include "vcpu.h"

/* #define APIC_DEBUG */
#include <core/printf.h>
#ifdef APIC_DEBUG
#define APIC_DBG(...)						\
	do {							\
		printf("APIC: " __VA_ARGS__);			\
	} while (0)
#else
#define APIC_DBG(...)
#endif

#define FAKE_SIPI_VECTOR 0xff

void
apic_read_cr8 (u64 *val)
{
#ifdef __x86_64__
	asm_rdcr8 (val);
#else
	panic ("apic_read_cr8 called on x86_32");
#endif
}

void
apic_write_cr8 (u64 val)
{
#ifdef __x86_64__
	asm_wrcr8 (val);
#else
	panic ("apic_write_cr8 called on x86_32");
#endif
}

void
apic_base_changed (ulong apic_base)
{
	if (apic_base != APIC_BASE_ADDRESS) {
		panic ("apic_base_changed to %08lx", apic_base);
	}
}

void
apic_enter_wait_for_sipi_state(void)
{
	spinlock_lock(&current->apic_data.lock);
	current->apic_data.sipi_pending = false;
	current->apic_data.sipi_vector = 0;
	current->apic_data.wait_for_sipi = true;
	spinlock_unlock(&current->apic_data.lock);
}

static int
fake_sipi_handler(vector_t vector, void *data)
{
	int ret = 0;

	spinlock_lock(&current->apic_data.lock);
	if (current->apic_data.fake_sipi) {
		current->apic_data.fake_sipi = false;
		ret = 1;
	}
	spinlock_unlock(&current->apic_data.lock);

	return ret;
}

void
apic_handle_wait_for_sipi_state (void)
{
	ASSERT(currentcpu->disable_interrupt == 1);

	spinlock_lock(&current->apic_data.lock);
	if (!current->apic_data.wait_for_sipi) {
		spinlock_unlock(&current->apic_data.lock);
		return;
	}
	
	APIC_DBG("handle wait-for-sipi state\n");
	apic_set_priority(ROUND_DOWN(FAKE_SIPI_VECTOR, 0x10) - 1);

	while (current->apic_data.wait_for_sipi) {
		if (!current->apic_data.sipi_pending) {
			/* Keep interrupts disabled on unlocking */
			spinlock_unlock(&current->apic_data.lock);

			/*
			 * Enable interrupts and halt.
			 * Please note execution of STI with RFLAGS.IF = 0
			 * blocks interrupts for one instruction after
			 * its execution.
			 */
			asm_sti_and_hlt();

			spinlock_lock(&current->apic_data.lock);
		}
		if (current->apic_data.sipi_pending != 0) {
			current->vmctl.write_realmode_seg(SREG_CS,
				  current->apic_data.sipi_vector << 8);
			current->vmctl.write_ip(0);
			current->apic_data.sipi_pending = false;
			current->apic_data.sipi_vector = 0;
			current->apic_data.wait_for_sipi = false;
		}
	}

	apic_set_priority(0x00);
	APIC_DBG("Exit wait-for-sipi state\n");
	spinlock_unlock(&current->apic_data.lock);
}

static void
send_init(struct vcpu *dest_vcpu)
{
	spinlock_lock(&dest_vcpu->apic_data.lock);
	if (dest_vcpu->apic_data.wait_for_sipi) {
		spinlock_unlock(&dest_vcpu->apic_data.lock);
		return;
	}
	spinlock_unlock(&dest_vcpu->apic_data.lock);
	apic_send_init(dest_vcpu->apic_id);
}

static bool
call_send_init(struct vcpu *dest_vcpu, void *data)
{
	if (dest_vcpu == current) {
		return false;
	}

	send_init(dest_vcpu);
	return false;
}

void
apic_send_init_others(void)
{
	vcpu_list_foreach(current->vm, call_send_init, NULL);
}

static void
send_sipi (struct vcpu *dest_vcpu, vector_t vector)
{
	spinlock_lock(&dest_vcpu->apic_data.lock);
	if (dest_vcpu->apic_data.wait_for_sipi) {
		dest_vcpu->apic_data.sipi_pending = true;
		dest_vcpu->apic_data.sipi_vector = vector;
		if (!dest_vcpu->apic_data.fake_sipi) {
			dest_vcpu->apic_data.fake_sipi = true;
			apic_send_ipi(FAKE_SIPI_VECTOR, dest_vcpu->apic_id);
		}
	}
	spinlock_unlock(&dest_vcpu->apic_data.lock);
}

static bool
call_send_sipi (struct vcpu *dest_vcpu, void *data)
{
	if (dest_vcpu == current) {
		return false;
	}

	vector_t vector;
	vector = *(vector_t*)data;
	send_sipi(dest_vcpu, vector);
	return false;
}

static bool
call_send_ipi (struct vcpu *dest_vcpu, void *data)
{
	if (dest_vcpu == current) {
		return false;
	}

	vector_t vector;
	vector = *(vector_t*)data;
	apic_send_ipi(vector, dest_vcpu->apic_id);
	return false;
}

static int
apic_mmio_ro(void *data, phys_t gphys, bool wr, void *buf,
	       uint len, u32 flags)
{
	if (wr) {
		APIC_DBG("Ignore writing to off 0x%llx\n",
			 gphys - APIC_BASE_ADDRESS);
	} else {
		mmio_memcpy(buf, apic_g2v(gphys), len);
	}
	return 1; /* emulated */
}

static int
apic_mmio_pass(void *data, phys_t gphys, bool wr, void *buf,
	       uint len, u32 flags)
{
	if (wr) {
		mmio_memcpy(apic_g2v(gphys), buf, len);
	} else {
		mmio_memcpy(buf, apic_g2v(gphys), len);
	}
	return 1; /* emulated */
}

static int
icr_low_handler(u32 *buf, bool wr)
{
	u32 icr_low;
	u32 icr_high;
	u32 delivery_mode;
	struct vcpu *dest_vcpu;
	vector_t vector;
	apic_id_t apic_id;
	u32 dsh;
	int emulated = 1;

	if (!wr) {
		*buf = (current->apic_data.icr_low & ~ICR_STATUS_BIT) |
			(apic_read32(APIC_ICR_LOW_OFFSET) & ICR_STATUS_BIT);
		return 1 /* emulated */;
	}

	icr_high = current->apic_data.icr_high;
	icr_low = current->apic_data.icr_low = *buf;

	delivery_mode = icr_low & ICR_DELIVERY_MODE_MASK;
	dsh = icr_low & ICR_DSH_MASK;

	switch (delivery_mode) {
	case ICR_DELIVERY_MODE_FIXED:
	case ICR_DELIVERY_MODE_LOWEST_PRI:
		switch (dsh) {
		case ICR_DSH_DEST:
#ifdef APIC_DEBUG
			if ((icr_high & ICR_DEST_MASK) == ICR_DEST_MASK) {
				APIC_DBG("Broadcast IPI: CPU%d ICR %08x %08x\n",
					 get_cpu_id(), icr_high, icr_low);
			}
#endif
			/* fall-through */
		case ICR_DSH_SELF:
			disable_interrupt();
			apic_wait_for_idle();
			apic_write32(APIC_ICR_HIGH_OFFSET, icr_high);
			apic_write32(APIC_ICR_LOW_OFFSET, icr_low);
			enable_interrupt();
			break;
		case ICR_DSH_OTHER:
			vector = apic_get_vector (icr_low);
			vcpu_list_foreach(current->vm, call_send_ipi,
					  &vector);
			break;
		default:
			APIC_DBG("Specified DSH is not supported on fixed int: CPU%d ICR %08x %08x\n",
				 get_cpu_id(), icr_high, icr_low);
		}
		break;
	case ICR_DELIVERY_MODE_NMI:
		APIC_DBG("NMI is not supported: CPU%d ICR %08x %08x\n",
			 get_cpu_id(), icr_high, icr_low);
		break;
	case ICR_DELIVERY_MODE_INIT:
		if ((icr_low & ICR_LEVEL_ASSERT) == 0) {
			break;
		}
		switch (dsh) {
		case ICR_DSH_DEST:
			if (apic_is_logical_dest_mode(icr_low)) {
				APIC_DBG("Logical dest mode is not supported on INIT: CPU%d ICR %08x %08x\n",
					 get_cpu_id(), icr_high, icr_low);
				break;
			}
			apic_id = apic_get_apic_id(icr_high);
			dest_vcpu = find_vcpu_with_apic_id(current->vm,
							   apic_id);
			if (!dest_vcpu) {
				break;
			}
			send_init(dest_vcpu);
			break;
		case ICR_DSH_OTHER:
			apic_send_init_others();
			break;
		default:
			APIC_DBG("Specified DSH is not supported on INIT: CPU%d ICR %08x %08x\n",
				 get_cpu_id(), icr_high, icr_low);
		} 
		break;
	case ICR_DELIVERY_MODE_STARTUP:
		switch (dsh) {
		case ICR_DSH_DEST:
			if (apic_is_logical_dest_mode(icr_low)) {
				APIC_DBG("Logical dest mode is not supported on SIPI: CPU%d ICR %08x %08x\n",
					 get_cpu_id(), icr_high, icr_low);
				break;
			}
			apic_id = apic_get_apic_id(icr_high);
			dest_vcpu = find_vcpu_with_apic_id(current->vm,
							   apic_id);
			if (!dest_vcpu) {
				break;
			}
			vector = apic_get_vector(icr_low);
			send_sipi(dest_vcpu, vector);
			break;
		case ICR_DSH_OTHER:
			vector = apic_get_vector (icr_low);
			vcpu_list_foreach(current->vm, call_send_sipi,
					  &vector);
			break;
		default:
			APIC_DBG("Specified DSH is not supported on SIPI: CPU%d ICR %08x %08x\n",
				 get_cpu_id(), icr_high, icr_low);
		}
		break;
	default:
		break;
	}
	return emulated;
}

static int
icr_high_handler(u32 *buf, bool wr)
{
	if (wr) {
		current->apic_data.icr_high = *buf;
	} else {
		*buf = current->apic_data.icr_high;
	}
	return 1; /* emulated */
}

static int
eoi_handler(u32 *buf, bool wr)
{
	if (!wr) {
		/* Read */
		return 0;
	}
	apic_write_eoi();
	return 1; /* emulated */
}

static int
logical_apic_id_handler(u32 *buf, bool wr)
{
	if (wr) {
		current->apic_data.ldr = *buf;
		APIC_DBG("Writing ldr 0x%x\n", current->apic_data.ldr);

	} else {
		*buf = current->apic_data.ldr;
		APIC_DBG("Reading ldr 0x%x\n", current->apic_data.ldr);
	}
	return 1; /* emulated */
}

static int
apic_privileged_mmio_handler(void *data, phys_t gphys, bool wr, void *buf,
			     uint len, u32 flags)
{
	int emulated = 0;

	if ((gphys & 0x3) != 0 || len != 4) {
		APIC_DBG("gphys 0x%llx len %d wr %d\n", gphys, len, wr);
		mmio_do_nothing(data, gphys, wr, buf, len, flags);
	}

	switch(gphys - APIC_BASE_ADDRESS) {
	case APIC_APIC_ID_OFFSET:
		emulated = apic_mmio_ro(data, gphys, wr, buf, len, flags);
		break;
	case APIC_EOI_OFFSET:
		emulated = eoi_handler(buf, wr);
		break;
	case APIC_ICR_LOW_OFFSET:
		emulated = icr_low_handler(buf, wr);
		break;
	case APIC_ICR_HIGH_OFFSET:
		emulated = icr_high_handler(buf, wr);
		break;
	}
	if (!emulated) {
		apic_mmio_pass(data, gphys, wr, buf, len, flags);
	}
	return 1; /* emulated */
}

static int
apic_mmio_handler(void *data, phys_t gphys, bool wr, void *buf,
			     uint len, u32 flags)
{
	int emulated = 0;

	if ((gphys & 0x3) != 0 || len != 4) {
		mmio_do_nothing(data, gphys, wr, buf, len, flags);
	}

	switch(gphys - APIC_BASE_ADDRESS) {
	case APIC_APIC_ID_OFFSET:
	case APIC_LVT_LINT0_OFFSET:
		emulated = apic_mmio_ro(data, gphys, wr, buf, len, flags);
		break;
	case APIC_EOI_OFFSET:
		emulated = eoi_handler(buf, wr);
		break;
	case APIC_LOGICAL_APIC_ID_OFFSET:
		emulated = logical_apic_id_handler(buf, wr);
		break;
	case APIC_ICR_LOW_OFFSET:
		emulated = icr_low_handler(buf, wr);
		break;
	case APIC_ICR_HIGH_OFFSET:
		emulated = icr_high_handler(buf, wr);
		break;
	}
	if (!emulated) {
		apic_mmio_pass(data, gphys, wr, buf, len, flags);
	}
	return 1; /* emulated */
}

static void
apic_pass_init(void)
{
	spinlock_init(&current->apic_data.lock);
	set_int_hook(FAKE_SIPI_VECTOR, fake_sipi_handler, NULL);
}

static void
apic_initvm(void)
{
	if (vm_get_id() == 0) {
		mmio_register(APIC_BASE_ADDRESS, APIC_MEM_SIZE,
			      apic_privileged_mmio_handler, NULL);
	} else {
		mmio_register(APIC_BASE_ADDRESS, APIC_MEM_SIZE,
			      apic_mmio_handler, NULL);
	}
}

INITFUNC ("passcpu0", apic_pass_init);
INITFUNC ("setupvm1", apic_initvm);
