/*
 * Copyright (c) 2010-2012 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 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/gmm.h>
#include <core/initfunc.h>
#include <core/io.h>
#include <core/mm.h>
#include <core/types.h>
#include <core/panic.h>
#include <core/printf.h>
#include <core/spinlock.h>
#include <core/string.h>
#include <core/vm.h>
#include <io/acpi_emu.h>
#include <io/io.h>

#define RTC_SECONDS		0x00
#define RTC_SECONDS_ALARM	0x01
#define RTC_MINUTES		0x02
#define RTC_MINUTES_ALARM	0x03
#define RTC_HOURS		0x04
#define RTC_HOURS_ALARM		0x05
#define RTC_DAY_OF_WEEK		0x06
#define RTC_DAY_OF_MONTH	0x07
#define RTC_MONTH		0x08
#define RTC_YEAR		0x09
#define RTC_REGISTER_A		0x0a
#define RTC_REGISTER_A_PASSTHROUGH	0x80;
#define RTC_REGISTER_B		0x0b
#define RTC_REGISTER_B_PASSTHROUGH	0x06;
#define RTC_REGISTER_C		0x0c
#define RTC_REGISTER_D		0x0d
#define RTC_REGISTER_D_PASSTHROUGH	0x80;
#define RTC_EXTMEM_LOW		0x30
#define RTC_EXTMEM_HIGH		0x31
#define RTC_CENTURY		0x32
#define RTC_EXTMEM2_LOW		0x34
#define RTC_EXTMEM2_HIGH	0x35
#define RTC_BIOS_BOOTFLAG1	0x38
#define RTC_BIOS_DISKTRANSFLAG	0x39
#define RTC_BIOS_BOOTFLAG2	0x3d
#define RTC_HIGHMEM_LOW		0x5b
#define RTC_HIGHMEM_MID		0x5c
#define RTC_HIGHMEM_HIGH	0x5d
#define RTC_BIOS_SMP_COUNT	0x5f
#define RTC_ACPI_PM_BASE	0x60

#define RTC_INDEX_REG_IOPORT		0x70
#define RTC_TARGET_REG_IOPORT		0x71
/*
 * We call "NMI disable bit" because 1 means NMI is disabled, while
 * Intel spec call "NMI enable bit".
 */
#define RTC_INDEX_REG_NMI_DISABLE_BIT	0x80
#define RTC_INDEX_REG_INDEX_MASK	0x7f

/* #define RTC_DEBUG */
#ifdef RTC_DEBUG
#define RTC_DBG(...)						\
	do {							\
		printf(__VA_ARGS__);				\
	} while (0)
#else
#define RTC_DBG(...)
#endif

struct rtc_emu {
	spinlock_t	lock;
	u8		index;
	bool		allow_host_access;
};

static bool		nmi_disable;
static drvdata_hdl_t	rtc_handle;
static spinlock_t	host_rtc_lock = SPINLOCK_INITIALIZER;

static void
read_rtc(u8 index, u8 *data)
{
	spinlock_lock(&host_rtc_lock);
	out8(RTC_INDEX_REG_IOPORT,
	     (nmi_disable ? RTC_INDEX_REG_NMI_DISABLE_BIT : 0) |
	     (index & RTC_INDEX_REG_INDEX_MASK));
	in8(RTC_TARGET_REG_IOPORT, data);
	spinlock_unlock(&host_rtc_lock);
}

static void
write_rtc(u8 index, u8 data)
{
	spinlock_lock(&host_rtc_lock);
	out8(RTC_INDEX_REG_IOPORT,
	     (nmi_disable ? RTC_INDEX_REG_NMI_DISABLE_BIT : 0) |
	     (index & RTC_INDEX_REG_INDEX_MASK));
	out8(RTC_TARGET_REG_IOPORT, data);
	spinlock_unlock(&host_rtc_lock);
}

static phys_t
low_mem_size(void)
{
	phys_t addr;

	addr = gmm_top_of_low_avail_mem();
	if (addr < 16*1024*1024) {
		return 0;
	}
	return addr + 1 - 16*1024*1024;
}

static phys_t
high_mem_size(void)
{
	phys_t addr;

	addr = gmm_top_of_high_avail_mem();
	if (addr < 0x100000000) {
		return 0;
	}
	return addr + 1 - 0x100000000;
}

static void
read_vrtc(u8 index, u8 *data)
{
	switch (index & RTC_INDEX_REG_INDEX_MASK) {
	case RTC_SECONDS:
	case RTC_MINUTES:
	case RTC_HOURS:
	case RTC_DAY_OF_WEEK:
	case RTC_DAY_OF_MONTH:
	case RTC_MONTH:
	case RTC_YEAR:
		read_rtc(index, data);
		break;
	case RTC_REGISTER_A:
		read_rtc(index, data);
		*data &= RTC_REGISTER_A_PASSTHROUGH;
		break;
	case RTC_REGISTER_B:
		read_rtc(index, data);
		*data &= RTC_REGISTER_B_PASSTHROUGH;
		break;
	case RTC_REGISTER_D:
		read_rtc(index, data);
		*data &= RTC_REGISTER_D_PASSTHROUGH;
		break;
	case RTC_EXTMEM2_LOW:
		*data = low_mem_size() >> 16;
		break;
	case RTC_EXTMEM2_HIGH:
		*data = low_mem_size() >> 24;
		break;
	case RTC_HIGHMEM_LOW:
		*data = high_mem_size() >> 16;
		break;
	case RTC_HIGHMEM_MID:
		*data = high_mem_size() >> 24;
		break;
	case RTC_HIGHMEM_HIGH:
		*data = high_mem_size() >> 32;
		break;
	case RTC_BIOS_SMP_COUNT:
		*data = vm_get_vcpu_count() - 1;
		break;
	case RTC_BIOS_BOOTFLAG1:
		*data = 0x00; /* Don't check floopy sig */
		break;
	case RTC_BIOS_BOOTFLAG2:
		*data = 0x23; /* Boot from CD-ROM first, then HDD */
		break;
	case RTC_ACPI_PM_BASE:
		*data = acpi_get_dev_base() >> 8;
		break;
	default:
		*data = 0;
		break;
	}
}

static void
rtc_init(void)
{
	u8	reg;

	in8(RTC_INDEX_REG_IOPORT, &reg);
	nmi_disable = (reg & RTC_INDEX_REG_NMI_DISABLE_BIT) ? true : false;
	RTC_DBG("nmi %s.\n", nmi_disable ? "disabled": "enabled");
	rtc_handle = vm_alloc_driver_data(sizeof(struct rtc_emu));
}

static int
rtc_index_handler(iotype_t type, ioport_t port, void *data)
{
	struct rtc_emu	*rtc_emu;
	bool		guest_nmi_disable;

	rtc_emu = vm_get_driver_data(rtc_handle);
	spinlock_lock(&rtc_emu->lock);

	switch (type) {
	case IOTYPE_INB:
		*(u8 *)data = rtc_emu->index;
		RTC_DBG("RTC index reg inb 0x%x\n",  *(u8 *)data);
		break;
	case IOTYPE_OUTB:
		RTC_DBG("RTC index reg outb 0x%x\n",  *(u8 *)data);
		rtc_emu->index = *(u8 *)data;
		if (!rtc_emu->allow_host_access) {
			break;
		}
		guest_nmi_disable =
			(rtc_emu->index & RTC_INDEX_REG_NMI_DISABLE_BIT) ?
			true : false;
		if (guest_nmi_disable == nmi_disable) {
			break;
		}
		spinlock_lock(&host_rtc_lock);
		nmi_disable = guest_nmi_disable;
		out8(RTC_INDEX_REG_IOPORT,
		     nmi_disable ? RTC_INDEX_REG_NMI_DISABLE_BIT : 0);
		RTC_DBG("nmi %s.\n", nmi_disable ? "disabled": "enabled");
		spinlock_unlock(&host_rtc_lock);
		break;
	default:
		break;
	}
	spinlock_unlock(&rtc_emu->lock);
	return 1; /* emulated */
}

static int
rtc_target_handler(iotype_t type, ioport_t port, void *data)
{
	struct rtc_emu	*rtc_emu;

	rtc_emu = vm_get_driver_data(rtc_handle);
	spinlock_lock(&rtc_emu->lock);

	switch (type) {
	case IOTYPE_INB:
		if (rtc_emu->allow_host_access) {
			read_rtc(rtc_emu->index, data);
		} else {
			read_vrtc(rtc_emu->index, data);
		}
		RTC_DBG("RTC target reg inb 0x%x\n",  *(u8 *)data);
	case IOTYPE_OUTB:
		RTC_DBG("RTC target reg outb 0x%x\n",  *(u8 *)data);
		if (rtc_emu->allow_host_access) {
			write_rtc(rtc_emu->index, *(u8 *)data);
		}
	default:
		break;
	}
	spinlock_unlock(&rtc_emu->lock);
	return 1; /* emulated */
}

static void
rtc_setup_vm(void)
{
	struct rtc_emu *rtc_emu;

	rtc_emu = vm_get_driver_data(rtc_handle);

	spinlock_init(&rtc_emu->lock);

	if (vm_get_id() == 0) {
		rtc_emu->allow_host_access = 1;
		rtc_emu->index
			= (nmi_disable ? RTC_INDEX_REG_NMI_DISABLE_BIT : 0);
	} else {
		rtc_emu->allow_host_access = 0;
		rtc_emu->index = 0;
	}

	set_iofunc(RTC_INDEX_REG_IOPORT, rtc_index_handler);
	set_iofunc(RTC_TARGET_REG_IOPORT, rtc_target_handler);
}

static void
rtc_reset(void)
{
	u8 reg;

	if (vm_get_id() != 0) {
		return;
	}
	printf("Reset RTC.\n");
	write_rtc(RTC_REGISTER_A, 0x26);
	read_rtc(RTC_REGISTER_B, &reg);
	write_rtc(RTC_REGISTER_B, (reg & ~0x04) | 0x02);
	read_rtc(RTC_REGISTER_C, &reg);
}

DRIVER_INIT(rtc_init);
DRIVER_SETUPVM(rtc_setup_vm);
DRIVER_RESET(rtc_reset);
