/*
 * 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.
 */
/*
 * PIT emulator with 3 counters.
 * Only support the following modes.
 *    mode 0 (out signal on end of count) to boot linux
 *    mode 2 (rate genarater) for system timer of bios
 *    mode 3 (square wave mode) for beep
 */
#include <core/assert.h>
#include <core/string.h>
#include <core/initfunc.h>
#include <core/io.h>
#include <core/mm.h>
#include <core/panic.h>
#include <core/printf.h>
#include <core/spinlock.h>
#include <core/time.h>
#include <core/types.h>
#include <core/vm.h>
#include <io/pic_emu.h>
#include "pit_regs.h"

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

#define SYSTEM_TIMER_IRQ			0

struct pit_counter {
	u8	no;
	u16	value;
	u16	init_val;
	u8	tcw;
	int	access_cnt;
	u64	last_time;
};

struct pit_emu {
	spinlock_t		lock;
	struct pit_counter	counter[PIT_COUNTER_NUM];
	u8			nmi_sc;
};

static drvdata_hdl_t	pit_handle;

static void
pit_write_counter(struct pit_counter *counter, u8 data)
{
	switch (counter->tcw & PIT_TCW_REG_RDWR_SELECT_MASK) {
	case PIT_TCW_REG_LSB:
		counter->init_val
			= (counter->init_val & 0xff00) |
			data;
		break;
	case PIT_TCW_REG_MSB:
		counter->init_val
			= (counter->init_val & 0x00ff) |
			(u16)data << 8;
		break;
	case PIT_TCW_REG_LSB_MSB:
		counter->access_cnt++;
		if (counter->access_cnt % 2) {
			counter->init_val
				= (counter->init_val & 0xff00) |
				data;
		} else {
			counter->init_val
				= (counter->init_val & 0x00ff) |
				(u16)data << 8;
		}
		break;
	}
	counter->value = counter->init_val;
	counter->last_time = get_cpu_time();
}

static u16
pit_usec_to_counter(u64 usec)
{
#ifndef __x86_64__
	u32 usec32;
#endif
	/*
	 * If we specify a value over than 78 sec, overflow will be
	 * occured.
	*/
	if (usec > 0xffffLL * PIT_COUNTER_RAIT / 1000000) {
		return 0xffff;
	}
#ifdef __x86_64__
	usec *= 1000000;
	usec /= PIT_COUNTER_RAIT;
#else
	usec32 = usec;
	usec32 *= 10000;
	usec32 /= (PIT_COUNTER_RAIT / 100);
#endif
	return usec;
}

static void
pit_update_counter(struct pit_counter *counter)
{
	u64 now;
	u16 new_value;

	now = get_cpu_time();
	new_value = counter->value
		- pit_usec_to_counter(now - counter->last_time);
	
	switch (counter->tcw & PIT_TCW_REG_MODE_SELECT_MASK) {
	case PIT_TCW_REG_MODE0:
		if (new_value > counter->value) {
			/* Overflowed */
			new_value = 0;
		}
		break;
	case PIT_TCW_REG_MODE2:
	case PIT_TCW_REG_MODE2A:
	case PIT_TCW_REG_MODE3:
		if (new_value > counter->value) {
			/* Overflowed */
			if (counter->no == 0) {
				picemu_trigger_boot_int(SYSTEM_TIMER_IRQ);
			}
			if (counter->init_val != 0) {
				while (new_value > counter->init_val) {
					new_value += counter->init_val;
				}
			}
		}
		break;
	}
	counter->value = new_value;
	counter->last_time = now;
}

static void
pit_read_counter(struct pit_counter *counter, u8 *data)
{
	pit_update_counter(counter);
	switch (counter->tcw & PIT_TCW_REG_RDWR_SELECT_MASK) {
	case PIT_TCW_REG_LSB:
		*data = counter->value & 0x00ff;
		break;
	case PIT_TCW_REG_MSB:
		*data = (counter->value & 0xff00) >> 8;
		break;
	case PIT_TCW_REG_LSB_MSB:
		counter->access_cnt++;
		if (counter->access_cnt % 2) {
			*data = counter->value & 0x00ff;
		} else {
			*data = (counter->value & 0xff00) >> 8;
		}
		break;
	default:
		*data = 0;
	}
}

static int
pit_counter_access_handler(iotype_t type, ioport_t port, void *data)
{
	struct pit_emu *pit_emu;

	pit_emu = vm_get_driver_data(pit_handle);
	spinlock_lock(&pit_emu->lock);

	switch(type) {
	case IOTYPE_OUTB:
		PITEMU_DBG("counter %d reg outb 0x%x\n",
			   port - PIT_COUNTER0_ACCESS_IOPORT,
			   (*(u8 *)data));
		pit_write_counter(pit_emu->counter
				  + (port - PIT_COUNTER0_ACCESS_IOPORT),
				  *(u8 *)data);
		break;
	case IOTYPE_INB:
		pit_read_counter(pit_emu->counter
				 + (port - PIT_COUNTER0_ACCESS_IOPORT),
				 (u8 *)data);

		PITEMU_DBG("counter %d reg inb 0x%x\n",
			   port - PIT_COUNTER0_ACCESS_IOPORT,
			   (*(u8 *)data));
		break;
	default:
		/* noting to do */
		PITEMU_DBG("counter %d reg access type %x\n",
			   port - PIT_COUNTER0_ACCESS_IOPORT,
			   type);
		break;
	}
	spinlock_unlock(&pit_emu->lock);
	return 1; /* emulated */
}

static void
pit_write_counter_tcw(struct pit_counter *counter, u8 data_u8)
{
	counter->access_cnt = 0;
	counter->tcw = data_u8;
	counter->init_val = 0;

	switch (counter->tcw & PIT_TCW_REG_MODE_SELECT_MASK) {
	case PIT_TCW_REG_MODE0:
	case PIT_TCW_REG_MODE2:
	case PIT_TCW_REG_MODE2A:
	case PIT_TCW_REG_MODE3:
		break;
	default:
		printf("Unsupported pit mode. tcw 0x%x\n",
		       counter->tcw);
		break;
	}
}

static int
pit_tcw_reg_handler(iotype_t type, ioport_t port, void *data)
{
	struct pit_emu *pit_emu;
	u8 data_u8;

	pit_emu = vm_get_driver_data(pit_handle);
	spinlock_lock(&pit_emu->lock);

	switch(type) {
	case IOTYPE_OUTB:
		PITEMU_DBG("TCW reg outb 0x%x\n", (*(u8 *)data));
		data_u8 = *(u8 *)data;
		if ((data_u8 & PIT_TCW_REG_RDWR_SELECT_MASK) ==
		    PIT_TCW_REG_COUNTER_LATCH_COMMAND) {
			break;
		}
		if ((data_u8 & PIT_TCW_REG_COUNTER_SELECT_MASK) ==
		    PIT_TCW_REG_READ_BACK_COMMAND) {
			break;
		}
		pit_write_counter_tcw(pit_emu->counter
		      + ((data_u8 & PIT_TCW_REG_COUNTER_SELECT_MASK)
			 >> PIT_TCW_REG_COUNTER_SELECT_SHIFT),
			data_u8);
		break;
	default:
		/* noting to do */
		PITEMU_DBG("TCW regaccess type %x\n", type);
		break;
	}
	spinlock_unlock(&pit_emu->lock);
	return 1; /* emulated */
}

static int
pit_nmi_sc_reg_handler(iotype_t type, ioport_t port, void *data)
{
	struct pit_emu *pit_emu;
	int counter2_out = 0;

	pit_emu = vm_get_driver_data(pit_handle);
	spinlock_lock(&pit_emu->lock);

	switch(type) {
	case IOTYPE_OUTB:
		pit_emu->nmi_sc = (*(u8 *)data) & PIT_NMI_SC_REG_CONTROL_MASK;
		PITEMU_DBG("NMI SC reg outb 0x%x\n", (*(u8 *)data));
		break;
	case IOTYPE_INB:
		if (pit_emu->nmi_sc & PIT_NMI_SC_REG_CONTROL_COUNTER2_EN) {
			pit_update_counter(pit_emu->counter + 2);
			counter2_out = (pit_emu->counter[2].value == 0);
		}
		*(u8 *)data = pit_emu->nmi_sc
			| (counter2_out ? PIT_NMI_SC_REG_TIMER_COUNTER2: 0);
		PITEMU_DBG("NMI SC reg inb 0x%x\n", (*(u8 *)data));
		break;
	default:
		/* noting to do */
		PITEMU_DBG("NMI SC reg access type %x\n", type);
		break;
	}
	spinlock_unlock(&pit_emu->lock);
	return 1; /* emulated */
}

void
pit_update_counter0(void)
{
	struct pit_emu *pit_emu;

	pit_emu = vm_get_driver_data(pit_handle);
	spinlock_lock(&pit_emu->lock);
	pit_update_counter(pit_emu->counter);
	spinlock_unlock(&pit_emu->lock);
}

static void
pit_setup_vm(void)
{
	struct pit_emu *pit_emu;
	int i;

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

	pit_emu = vm_get_driver_data(pit_handle);
	spinlock_init(&pit_emu->lock);
	for (i = 0; i < PIT_COUNTER_NUM; i++) {
		pit_emu->counter[i].no = i;
	}

	set_iofunc(PIT_COUNTER0_ACCESS_IOPORT, pit_counter_access_handler);
	set_iofunc(PIT_COUNTER1_ACCESS_IOPORT, pit_counter_access_handler);
	set_iofunc(PIT_COUNTER2_ACCESS_IOPORT, pit_counter_access_handler);
	set_iofunc(PIT_TCW_REG_IOPORT, pit_tcw_reg_handler);
	set_iofunc(PIT_NMI_SC_REG_IOPORT, pit_nmi_sc_reg_handler);
}

static void
pit_init(void)
{
	pit_handle = vm_alloc_driver_data(sizeof(struct pit_emu));
}

DRIVER_SETUPVM(pit_setup_vm);
DRIVER_INIT(pit_init);
