/*
 * 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 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.
 */

/*
 * 8259A programable interrupt controller emulator.
 * Supports two EOI modes.
 *     Non-specific EOI
 *     Specific EOI
 *     Auto EOI
 */
#include <core/extint.h>
#include <core/initfunc.h>
#include <core/io.h>
#include <core/printf.h>
#include <core/spinlock.h>
#include <core/types.h>
#include <core/timer.h>
#include <core/vm.h>
#include <core/vmmerr.h>
#include <io/pic.h>
#include <io/pic_emu.h>
#include "pit_emu.h"

#define PIC_MASTER	0
#define PIC_SLAVE	1
#define PIC_CHIP_NUM	2
#define PIC_IRQ_NUM	8
#define PIC_CASCADE_IRQ	2

#define PICEMU_BOOT_INT_TIMER_INTERVAL 10000 /* 10ms */

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

#define PIC_CHIP_NO(irq) ((irq) / PIC_IRQ_NUM)
#define PIC_IR_BIT(irq) (1 << ((irq) % PIC_IRQ_NUM))

typedef enum pic_state {
	PIC_NOT_INITIALIZED = 0,
	PIC_ICW1_ACCEPTED,
	PIC_ICW2_ACCEPTED,
	PIC_ICW3_ACCEPTED,
	PIC_OPERATING
} pic_state_t;

struct pic_chip {
	pic_state_t	state;
	vector_t	vector_base;
	u8		icw4;
	u8		ocw3;
	u8		imr;
	u8		irr;
	u8		isr;
	u8		no;
};

struct pic_emu {
	spinlock_t	lock;
	void		*timer_handle;
	bool		disable_int_emu;
	struct pic_chip	pic_chip[PIC_CHIP_NUM];
};

static drvdata_hdl_t pic_handle;

/*
 * BIT scan forward.
 * Find the least significant set bit.
 */
static inline u8
asm_bsf_u8 (u8 val)
{
	u32 val32 = val;
	u32 bit;
	asm volatile ("bsfl %1,%0\n"
		      : "=r" (bit)
		      : "r" (val32));
	return bit;
}

static vmmerr_t
picemu_irq2vector(struct pic_emu *pic, irq_t irq, vector_t *vector)
{
	int		chip_no;

	chip_no = PIC_CHIP_NO(irq);
	if (chip_no >= PIC_CHIP_NUM) {
		return VMMERR_RANGE;
	}
	*vector = pic->pic_chip[chip_no].vector_base
		+ (irq - chip_no * PIC_IRQ_NUM);
	return VMMERR_SUCCESS;
}

vmmerr_t
picemu_get_vector(irq_t irq, vector_t *vector)
{
	struct pic_emu	*pic;
	vmmerr_t	ret;

	pic = vm_get_driver_data(pic_handle);
	spinlock_lock(&pic->lock);
	ret = picemu_irq2vector(pic, irq, vector);
	spinlock_unlock(&pic->lock);
	return ret;
}

static void
picemu_next_int(struct pic_emu *pic)
{
	vector_t	vector;
	struct pic_chip *chip;
	u8		ir_bit;
	u8		masked_irr;
	irq_t		irq;
	irq_t		slave_irq = INVALID_IRQ;
	irq_t		is_irq;

	chip = pic->pic_chip + PIC_SLAVE;

	masked_irr = chip->irr & (~chip->imr);
	if (masked_irr == 0) {
		goto master;
	}
	irq = asm_bsf_u8(masked_irr);
	if (chip->isr) {
		is_irq = asm_bsf_u8(chip->isr);
		if (irq >= is_irq) {
			goto master;
		}
	}
	ir_bit = 1 << irq;
	chip->irr &= ~ir_bit;
	if ((chip->icw4 & PIC_ICW4_AUTO_EOI) == 0) {
		chip->isr |= ir_bit;
	}

	slave_irq = irq + PIC_IRQ_NUM;

master:
	chip = pic->pic_chip + PIC_MASTER;

	masked_irr = chip->irr & (~chip->imr);
	if (masked_irr == 0) {
		return;
	}
	irq = asm_bsf_u8(masked_irr);
	if (chip->isr) {
		is_irq = asm_bsf_u8(chip->isr);
		if (irq >= is_irq) {
			return;
		}
	}
	ir_bit = 1 << irq;
	chip->irr &= ~ir_bit;
	if ((chip->icw4 & PIC_ICW4_AUTO_EOI) == 0) {
		chip->isr |= ir_bit;
	}

	if (irq == PIC_CASCADE_IRQ) {
		irq = slave_irq;
	}

	if (picemu_irq2vector(pic, irq, &vector) == VMMERR_SUCCESS) {
		extint_pend_8259a_int(vector);
	}
}

static void
picemu_do_trigger_int(struct pic_emu *pic, irq_t irq)
{
	struct pic_chip *chip;
	int		chip_no;

	chip_no = PIC_CHIP_NO(irq);
	if (chip_no >= PIC_CHIP_NUM) {
		printf("picemu_do_trigger_int: "
		       "Out of range of irq. irq %d\n",
		       irq);
		return;
	}

	chip = pic->pic_chip + chip_no;
	if (chip->state == PIC_NOT_INITIALIZED) {
		return;
	}

	chip->irr |= PIC_IR_BIT(irq);
	picemu_next_int(pic);
}

void
picemu_trigger_int(irq_t irq)
{
	struct pic_emu	*pic;

	PICEMU_DBG("PIC: Trigger %d\n", irq);
	pic = vm_get_driver_data(pic_handle);
	spinlock_lock(&pic->lock);
	picemu_do_trigger_int(pic, irq);
	spinlock_unlock(&pic->lock);
}

void
picemu_trigger_boot_int(irq_t irq)
{
	struct pic_emu	*pic;

	pic = vm_get_driver_data(pic_handle);

	spinlock_lock(&pic->lock);
	if (pic->disable_int_emu) {
		spinlock_unlock(&pic->lock);
		return;
	}
	picemu_do_trigger_int(pic, irq);

	spinlock_unlock(&pic->lock);
}

static u8
picemu_cmd_read(struct pic_chip *pic_chip)
{
	/*
	 * Return ISR value or IRR value.
	 * Currently ,return 0 regardless of selecting ISR or IRR by OCW3.
	 */
	PICEMU_DBG("PIC%d Read ISR/IRR\n", pic_chip->no);
	if ((pic_chip->ocw3 & PIC_OCW3_READ_REG_MASK)
	    == PIC_OCW3_READ_IRR) {
		return pic_chip->irr;
	} else if ((pic_chip->ocw3 & PIC_OCW3_READ_REG_MASK)
	    == PIC_OCW3_READ_ISR) {
		return pic_chip->isr;
	}
	return 0;
}

static void
picemu_cmd_write(struct pic_emu *pic, struct pic_chip *pic_chip, u8 data)
{
	irq_t irq;

	if ((data & PIC_ICW1_SELECT_MASK) == PIC_ICW1_SELECT_MATCH) {
		/* ICW1 */
		pic->disable_int_emu = true;
		pic_chip->imr = 0;
		pic_chip->icw4 = 0;
		pic_chip->ocw3 = 0;
		pic_chip->irr = 0;
		pic_chip->isr = 0;
		pic_chip->state = PIC_ICW1_ACCEPTED;
		extint_reset_8259a();
		PICEMU_DBG("PIC%d ICW1\n", pic_chip->no);
	} else if ((data & PIC_NON_SPECIFIC_EOI_MASK) ==
		   PIC_NON_SPECIFIC_EOI_MATCH) {
		if (pic_chip->isr) {
			irq = asm_bsf_u8(pic_chip->isr);
			pic_chip->isr &= ~(1 << irq);
		}
		PICEMU_DBG("PIC%d Non-Specific EOI\n", pic_chip->no);
		picemu_next_int(pic);
	} else if ((data & PIC_SPECIFIC_EOI_MASK) ==
		   PIC_SPECIFIC_EOI_MATCH) {
		irq = data & PIC_EOI_IRQ_MASK;
		pic_chip->isr &= ~(1 << irq);
		PICEMU_DBG("PIC%d Specific-EOI\n", pic_chip->no);
		picemu_next_int(pic);
	} else if ((data & PIC_OCW3_SELECT_MASK) ==
		   PIC_OCW3_SELECT_MATCH) {
		pic_chip->ocw3 = data;
		PICEMU_DBG("PIC%d OCW3\n", pic_chip->no);
#ifdef PICEMU_DEBUG
	} else {
		PICEMU_DBG("PIC%d Unknown command 0x%x\n", pic_chip->no, data);
#endif
	}
}

static int
picemu_cmd(iotype_t type, ioport_t port, void *data)
{
	struct pic_emu	*pic;
	struct pic_chip	*pic_chip;

	pic = vm_get_driver_data(pic_handle);
	spinlock_lock(&pic->lock);
	if (port == PIC_MASTER_CMD_PORT) {
		pic_chip = &pic->pic_chip[PIC_MASTER];
	} else {
		pic_chip = &pic->pic_chip[PIC_SLAVE];
	}

	switch (type) {
	case IOTYPE_OUTB:
		picemu_cmd_write(pic, pic_chip, *(u8 *)data);
		break;
	case IOTYPE_INB:
		*(u8 *)data = picemu_cmd_read(pic_chip);
		break;
	default:
		break;
	}
	spinlock_unlock(&pic->lock);
	return 1; /* emulated */
}

static void
picemu_boot_int_timer(void *handle, void *data)
{
	struct pic_emu	*pic = (struct pic_emu *)data;

	spinlock_lock(&pic->lock);
	if (pic->disable_int_emu) {
		spinlock_unlock(&pic->lock);
		return;
	}
	spinlock_unlock(&pic->lock);

	/*
	 * Emulate system timer interrupts.
	 */
	pit_update_counter0();

	spinlock_lock(&pic->lock);
	timer_set(handle, PICEMU_BOOT_INT_TIMER_INTERVAL);
	spinlock_unlock(&pic->lock);
}

static void
picemu_imr_write(struct pic_emu *pic, struct pic_chip *pic_chip, u8 data,
		 void *timer_handle)
{
	switch (pic_chip->state) {
	case PIC_ICW1_ACCEPTED:
		/* ICW2 */
		pic_chip->state = PIC_ICW2_ACCEPTED;
		pic_chip->vector_base = (data & PIC_ICW2_VECTOR_MASK);
		PICEMU_DBG("PIC%d ICW2 vector_base 0x%x\n",
			   pic_chip->no, pic_chip->vector_base);
		break;
	case PIC_ICW2_ACCEPTED:
		/* ICW3 */
		pic_chip->state = PIC_ICW3_ACCEPTED;
		PICEMU_DBG("PIC%d ICW3\n", pic_chip->no);
		break;
	case PIC_ICW3_ACCEPTED:
		/* ICW4 */
		pic_chip->state = PIC_OPERATING;
		pic_chip->icw4 = data;
		PICEMU_DBG("PIC%d ICW4 %s\n", pic_chip->no,
			   (data & PIC_ICW4_AUTO_EOI ?
			    "Auto EOI": "Non-Auto EOI"));
		break;
	default:
		/* OCW1 */
		pic_chip->imr = data;
		PICEMU_DBG("PIC%d OCW1 IMR 0x%x\n",
			   pic_chip->no, pic_chip->imr);
		picemu_next_int(pic);

		if ((pic_chip->no == PIC_CHIP_NO(0) &&
		     (pic_chip->imr & PIC_IR_BIT(0)) == 0)) {
			/*
			 * Assigned devices's interrupt or timer
			 * interrupt is enabled.
			 */
			timer_set(timer_handle, PICEMU_BOOT_INT_TIMER_INTERVAL);
			pic->disable_int_emu = false;
		} else {
			pic->disable_int_emu = true;
		}
		break;
	}
}

static u8
picemu_imr_read(struct pic_chip *pic_chip)
{
	PICEMU_DBG("PIC%d Read IMR 0x%x\n", pic_chip->no, pic_chip->imr);
	return pic_chip->imr;
}

static int
picemu_imr(iotype_t type, ioport_t port, void *data)
{
	struct pic_emu	*pic;
	struct pic_chip	*pic_chip;

	pic = vm_get_driver_data(pic_handle);

	spinlock_lock(&pic->lock);
	if (port == PIC_MASTER_IMR_PORT) {
		pic_chip = &pic->pic_chip[PIC_MASTER];
	} else {
		pic_chip = &pic->pic_chip[PIC_SLAVE];
	}

	switch (type) {
	case IOTYPE_OUTB:
		picemu_imr_write(pic, pic_chip, *(u8 *)data, pic->timer_handle);
		break;
	case IOTYPE_INB:
		*(u8 *)data = picemu_imr_read(pic_chip);
		break;
	default:
		break;
	}
	spinlock_unlock(&pic->lock);
	return 1; /* emulated */
}

static void
picemu_setup (void)
{
	struct pic_emu	*pic;
	int i;

	pic = vm_get_driver_data(pic_handle);

	spinlock_init(&pic->lock);
	for (i = 0; i < PIC_CHIP_NUM; i++) {
		pic->pic_chip[i].no = i;
	}

	pic->timer_handle = timer_new(picemu_boot_int_timer, pic);
	pic->disable_int_emu = true;

	if (vm_get_id() == 0) {
		/*
		 * vm0 is allowed to access pic directory.
		 * See core/pic.c.
		 */
		return;
	}

	set_iofunc(PIC_MASTER_CMD_PORT, picemu_cmd);
	set_iofunc(PIC_SLAVE_CMD_PORT, picemu_cmd);
	set_iofunc(PIC_MASTER_IMR_PORT, picemu_imr);
	set_iofunc(PIC_SLAVE_IMR_PORT, picemu_imr);
}

static void
picemu_init(void)
{
	pic_handle = vm_alloc_driver_data(sizeof(struct pic_emu));
}

DRIVER_SETUPVM(picemu_setup);
DRIVER_INIT(picemu_init);
