/*
    JPC: A x86 PC Hardware Emulator for a pure Java Virtual Machine
    Release Version 2.0

    A project from the Physics Dept, The University of Oxford

    Copyright (C) 2007 Isis Innovation Limited

    This program is free software; you can redistribute it and/or modify
    it under the terms of the GNU General Public License version 2 as published by
    the Free Software Foundation.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License along
    with this program; if not, write to the Free Software Foundation, Inc.,
    51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 
    Details (including contact information) can be found at: 

    www.physics.ox.ac.uk/jpc
*/

package org.jpc.emulator.motherboard;

import org.jpc.emulator.processor.*;
import org.jpc.emulator.HardwareComponent;

/**
 * i8259 Programmable Interrupt Controller emulation.
 */
public class InterruptController implements IOPortCapable, HardwareComponent
{
    private InterruptControllerElement master;
    private InterruptControllerElement slave;

    private Processor connectedCPU;

    public InterruptController()
    {
	ioportRegistered = false;
	master = new InterruptControllerElement(true);
	slave = new InterruptControllerElement(false);
    }

    private void updateIRQ()
    {
	int slaveIRQ, masterIRQ;
	/* first look at slave irq */
	slaveIRQ = slave.getIRQ();
	if (slaveIRQ >= 0) {
	    /* if irq request by slave pic, signal Master PIC */
	    master.setIRQ(2,1);
	    master.setIRQ(2,0);
	}
	/* look at requested IRQ */
	masterIRQ = master.getIRQ();
	if(masterIRQ >= 0) {
	    connectedCPU.raiseInterrupt();
	}
    }

    public void setIRQ(int irqNumber, int level)
    {
	switch (irqNumber >> 3) {
	case 0: //master
	    master.setIRQ(irqNumber & 7, level);
	    this.updateIRQ();
	    break;
	case 1: //slave
	    slave.setIRQ(irqNumber & 7, level);
	    this.updateIRQ();
	    break;
	default:
	}
    }

    public int cpuGetInterrupt()
    {
	int masterIRQ, slaveIRQ;

	/* read the irq from the PIC */

	masterIRQ = master.getIRQ();
	if (masterIRQ >= 0) {
	    master.intAck(masterIRQ);
	    if (masterIRQ == 2) {
		slaveIRQ = slave.getIRQ();
		if (slaveIRQ >= 0) {
		    slave.intAck(slaveIRQ);
		} else {
		    /* spurious IRQ on slave controller */
		    slaveIRQ = 7;
		}
		this.updateIRQ();
		return (0xff & slave.getIRQBase()) + slaveIRQ;
		//masterIRQ = slaveIRQ + 8;
	    } else {
		this.updateIRQ();
		return (0xff & master.getIRQBase()) + masterIRQ;
	    }
	} else {
	    /* spurious IRQ on host controller */
	    masterIRQ = 7;
	    this.updateIRQ();
	    return (0xff & master.getIRQBase()) + masterIRQ;
	}
    }

    private int intAckRead()
    {
	int ret = master.pollRead(0x00);
	if (ret == 2)
	    ret = slave.pollRead(0x80) + 8;
	master.setReadRegisterSelect((byte)1);

	return ret;
    }

    private class InterruptControllerElement
    {
	private byte lastInterruptRequestRegister; //edge detection
	private byte interruptRequestRegister;
	private byte interruptMaskRegister;
	private byte interruptServiceRegister;

	private byte priorityAdd; // highest IRQ priority
	private byte irqBase;
	private byte readRegisterSelect;
	private byte poll;
	private byte specialMask;
	private byte initState;
	private byte autoEOI;
	private byte rotateOnAutoEOI;
	private byte specialFullyNestedMode;
	private byte init4; /* true if 4 byte init */
	private byte elcr; //(elcr) PIIX3 edge/level trigger selection
	private byte elcrMask;

	private int[] ioPorts;

	public InterruptControllerElement(boolean master)
	{
	    if (master == true) {
		ioPorts = new int[]{0x20, 0x21, 0x4d0};
		elcrMask = (byte)0xf8;
	    } else {
		ioPorts = new int[]{0xa0, 0xa1, 0x4d1};
		elcrMask = (byte)0xde;
	    }
	}

	/* BEGIN IOPortCapable Methods */
	public int[] ioPortsRequested()
	{
	    return ioPorts;
	}

	public byte ioPortRead(int address)
	{
	    int addressCopy;
	    byte ret;
	    addressCopy = address;
	    addressCopy &= 1;
	    if(0 != this.getPoll()) {
		ret = (byte)this.pollRead(address);
		this.setPoll((byte)0);
	    } else {
		if (addressCopy == 0) {
		    if (0 != this.getReadRegisterSelect()) {
			ret = this.getInterruptServiceRegister();
		    } else {
			ret = this.getInterruptRequestRegister();
		    }
		} else {
		    ret = this.getInterruptMaskRegister();
		}
	    }
	    return ret;
	}

	public byte elcrRead()
	{
	    return this.getELCR();
	}

	public boolean ioPortWrite(int address, byte data) //t/f updateIRQ
	{
	    int priority, command, irq;
	    address &= 1;
	    if (address == 0) {
		if (0 != (data & 0x10)) 
                {
		    /* init */
		    this.reset();
		    connectedCPU.clearInterrupt();

		    this.setInitState((byte)1);
		    this.setInit4((byte)(data & 1));
		    if (0 != (data & 0x02))
			System.err.println("single mode not supported");
		    if (0 != (data & 0x08))
			System.err.println("level sensitive irq not supported");
		} 
                else if (0 != (data & 0x08)) 
                {
		    if (0 != (data & 0x04))
			this.setPoll((byte)1);
		    if (0 != (data & 0x02))
			this.setReadRegisterSelect((byte)(data & 0x01));
		    if (0 != (data & 0x40))
			this.setSpecialMask((byte)((data >> 5) & 1));
		} 
                else 
                {
		    command = data >> 5;
		    switch(command) {
		    case 0:
		    case 4:
			this.setRotateOnAutoEOI((byte)(command >> 2));
			break;
		    case 1: // end of interrupt
		    case 5:
			priority = this.getPriority(this.getInterruptServiceRegister());
			if (priority != 8) {
			    irq = (priority + this.getPriorityAdd()) & 7;
			    this.andInterruptServiceRegister((byte)(~(1 << irq)));
			    if (command == 5)
				this.setPriorityAdd((byte)((irq + 1) & 7));
			    return true;
			}
			break;
		    case 3:
			irq = data & 7;
			this.andInterruptServiceRegister((byte)~(1 << irq));
			return true;
		    case 6:
			this.setPriorityAdd((byte)((data + 1) & 7));
			return true;
		    case 7:
			irq = data & 7;
			this.andInterruptServiceRegister((byte)~(1 << irq));
			this.setPriorityAdd((byte)((irq + 1) & 7));
			return true;
		    default:
			/* no operation */
			break;
		    }
		}
	    } 
            else 
            {
		switch(this.getInitState()) 
                {
		case 0:
		    /* normal mode */
		    this.setInterruptMaskRegister(data);
		    return true;
		case 1:
		    this.setIRQBase((byte)(data & 0xf8));
		    this.setInitState((byte)2);
		    break;
		case 2:
		    if (0 != this.getInit4()) {
			this.setInitState((byte)3);
		    } else {
			this.setInitState((byte)0);
		    }
		    break;
		case 3:
		    this.setSpecialFullyNestedMode((byte)((data >> 4) & 1));
		    this.setAutoEOI((byte)((data >> 1) & 1));
		    this.setInitState((byte)0);
		    break;
		}
	    }
	    return false;
	}

	public void elcrWrite(byte data)
	{
	    this.setELCR((byte)(data & this.getELCRMask()));
	}
	/* END IOPortCapable Methods */

	private int pollRead(int address)
	{
	    int ret = this.getIRQ();
	    if (ret < 0) {
		InterruptController.this.updateIRQ();
		return 0x07;
	    }
	    
	    if (0 != (address >> 7)) {
		InterruptController.this.masterPollCode();
	    }
	    this.andInterruptRequestRegister((byte)~(1 << ret));
	    this.andInterruptServiceRegister((byte)~(1 << ret));
	    if (0 != (address >> 7) || ret != 2)
		InterruptController.this.updateIRQ();
	    return ret;
	}

	public void setIRQ(int irqNumber, int level)
	{

	    int mask;
	    mask = (1 << irqNumber);
	    if(0 != (this.getELCR() & mask)) {
		/* level triggered */
		if (0 != level) {
		    this.orInterruptRequestRegister((byte)mask);
		    this.orLastInterruptRequestRegister((byte)mask);
		} else {
		    this.andInterruptRequestRegister((byte)~mask);
		    this.andLastInterruptRequestRegister((byte)~mask);
		}
	    } else {
		/* edge triggered */
		if (0 != level) {
		    if ((this.getLastInterruptRequestRegister() & mask) == 0) {
			this.orInterruptRequestRegister((byte)mask);
		    }
		    this.orLastInterruptRequestRegister((byte)mask);
		} else {
		    this.andLastInterruptRequestRegister((byte)~mask);
		}
	    }
	}

	private int getPriority(int mask)
	{
	    if ((0xff & mask) == 0) {
		return 8;
	    }
	    int priority = 0;
	    while ((mask & (1 << ((priority + this.getPriorityAdd()) & 7))) == 0) {
		priority++;
	    }
	    return priority;
	}

	public int getIRQ()
	{
	    int mask, currentPriority, priority;
	    
	    mask = this.getInterruptRequestRegister() & ~this.getInterruptMaskRegister();
	    priority = this.getPriority(mask);
	    if (priority == 8) {
		return -1;
	    }
	    /* compute current priority. If special fully nested mode on
	       the master, the IRQ coming from the slave is not taken into
	       account for the priority computation. */
	    mask = this.getInterruptServiceRegister();
	    if ((0 != this.getSpecialFullyNestedMode()) && this.isMaster()) {
		mask &= ~(1 << 2);
	    }
	    currentPriority = this.getPriority(mask);

	    if (priority < currentPriority) {
		/* higher priority found: an irq should be generated */
		return (priority + this.getPriorityAdd()) & 7;
	    } else {
		return -1;
	    }
	}

	private void intAck(int irqNumber)
	{
	    if (0 != this.getAutoEOI()) {
		if (0 != this.getRotateOnAutoEOI())
		    this.setPriorityAdd((byte)((irqNumber + 1) & 7));
	    } else {
		this.orInterruptServiceRegister((byte)(1 << irqNumber));
	    }
	    /* We don't clear a level sensitive interrupt here */
	    if (0 == (this.getELCR() & (1 << irqNumber)))
		this.andInterruptRequestRegister((byte)~(1 << irqNumber));
	}

	private boolean isMaster()
	{
	    if (InterruptController.this.master == this)
		return true;
	    else
		return false;
	}
	private void reset()
	{
	    //zero all variables except elcrMask
	    lastInterruptRequestRegister = (byte)0x0;
	    interruptRequestRegister = (byte)0x0;
	    interruptMaskRegister = (byte)0x0;
	    interruptServiceRegister = (byte)0x0;
	    
	    priorityAdd = (byte)0x0;
	    irqBase = (byte)0x0;
	    readRegisterSelect = (byte)0x0;
	    poll = (byte)0x0;
	    specialMask = (byte)0x0;
	    initState = (byte)0x0;
	    autoEOI = (byte)0x0;
	    rotateOnAutoEOI = (byte)0x0;
	    specialFullyNestedMode = (byte)0x0;
	    init4 = (byte)0x0; /* true if 4 byte init */
	    elcr = (byte)0x0; //(elcr) PIIX3 edge/level trigger selection
	}

	public byte getLastInterruptRequestRegister()
	{return lastInterruptRequestRegister;}
	public void setLastInterruptRequestRegister(byte newRegister)
	{lastInterruptRequestRegister = newRegister;}
	public void andLastInterruptRequestRegister(byte mask)
	{this.setLastInterruptRequestRegister((byte)(this.getLastInterruptRequestRegister() & mask));}
	public void orLastInterruptRequestRegister(byte mask)
	{this.setLastInterruptRequestRegister((byte)(this.getLastInterruptRequestRegister() | mask));}

	public byte getInterruptRequestRegister()
	{return interruptRequestRegister;}
	public void setInterruptRequestRegister(byte newRegister)
	{interruptRequestRegister = newRegister;}
	public void andInterruptRequestRegister(byte mask)
	{this.setInterruptRequestRegister((byte)(this.getInterruptRequestRegister() & mask));}
	public void orInterruptRequestRegister(byte mask)
	{this.setInterruptRequestRegister((byte)(this.getInterruptRequestRegister() | mask));}

	public byte getInterruptMaskRegister()
	{return interruptMaskRegister;}
	public void setInterruptMaskRegister(byte newRegister)
        {interruptMaskRegister = newRegister;}
	public void andInterruptMaskRegister(byte mask)
	{this.setInterruptMaskRegister((byte)(this.getInterruptMaskRegister() & mask));}
	public void orInterruptMaskRegister(byte mask)
	{this.setInterruptMaskRegister((byte)(this.getInterruptMaskRegister() | mask));}

	public byte getInterruptServiceRegister()
	{return interruptServiceRegister;}
	public void setInterruptServiceRegister(byte newRegister)
	{interruptServiceRegister = newRegister;}
	public void andInterruptServiceRegister(byte mask)
	{this.setInterruptServiceRegister((byte)(this.getInterruptServiceRegister() & mask));}
	public void orInterruptServiceRegister(byte mask)
	{this.setInterruptServiceRegister((byte)(this.getInterruptServiceRegister() | mask));}

	public byte getReadRegisterSelect()
	{return readRegisterSelect;}
	public void setReadRegisterSelect(byte newRegister)
	{readRegisterSelect = newRegister;}
	public void andReadRegisterSelect(byte mask)
	{this.setReadRegisterSelect((byte)(this.getReadRegisterSelect() & mask));}
	public void orReadRegisterSelect(byte mask)
	{this.setReadRegisterSelect((byte)(this.getReadRegisterSelect() | mask));}

	public byte getPriorityAdd()
	{return priorityAdd;}
	public void setPriorityAdd(byte newPriorityAdd)
	{priorityAdd = newPriorityAdd;}

	public byte getIRQBase()
	{return irqBase;}
	public void setIRQBase(byte newIRQBase)
	{
	    irqBase = newIRQBase;
	}

	public byte getPoll()
	{return poll;}
	public void setPoll(byte newPoll)
	{poll = newPoll;}

	public void setSpecialMask(byte newSpecialMask)
	{specialMask = newSpecialMask;}

	public byte getInitState()
	{return initState;}
	public void setInitState(byte newInitState)
	{initState = newInitState;}

	public byte getAutoEOI()
	{return autoEOI;}
	public void setAutoEOI(byte newAutoEOI)
	{autoEOI = newAutoEOI;}

	public byte getRotateOnAutoEOI()
	{return rotateOnAutoEOI;}
	public void setRotateOnAutoEOI(byte newRotateOnAutoEOI)
	{rotateOnAutoEOI = newRotateOnAutoEOI;}

	public byte getSpecialFullyNestedMode()
	{return specialFullyNestedMode;}
	public void setSpecialFullyNestedMode(byte newSpecialFullyNestedMode)
	{specialFullyNestedMode = newSpecialFullyNestedMode;}

	public byte getInit4()
	{return init4;}
	public void setInit4(byte newInit4)
	{init4 = newInit4;}

	public byte getELCR()
	{return elcr;}
	public void setELCR(byte newELCR)
	{elcr = newELCR;}

	public byte getELCRMask()
	{return elcrMask;}

	public String toString()
	{
	    if (isMaster()) {
		return (InterruptController.this).toString() + ": [Master Element]";
	    } else {
		return (InterruptController.this).toString() + ": [Slave  Element]";
	    }
	}
    }


    /* BEGIN IOPortCapable Defined Methods */
    public int[] ioPortsRequested()
    {
	int[] masterIOPorts = master.ioPortsRequested();
	int[] slaveIOPorts = slave.ioPortsRequested();

	int[] temp = new int[masterIOPorts.length + slaveIOPorts.length];
	System.arraycopy(masterIOPorts, 0, temp, 0, masterIOPorts.length);
	System.arraycopy(slaveIOPorts, 0, temp, masterIOPorts.length, slaveIOPorts.length);

	return temp;
    }

    public int ioPortReadByte(int address)
    {
	switch (address) {
	case 0x20:
	case 0x21:
	    return 0xff & master.ioPortRead(address);
	case 0xa0:
	case 0xa1:
	    return 0xff & slave.ioPortRead(address);
	case 0x4d0:
	    return 0xff & master.elcrRead();
	case 0x4d1:
	    return 0xff & slave.elcrRead();
	default:
	}
	return 0;
    }
    public int ioPortReadWord(int address)
    {
	return (0xff & ioPortReadByte(address)) |
	    (0xff00 & (ioPortReadByte(address + 1) << 8));
    }
    public int ioPortReadLong(int address)
    {
	return (0xffff & ioPortReadWord(address)) |
	    (0xffff0000 & (ioPortReadWord(address + 2) << 16));
    }

    public void ioPortWriteByte(int address, int data)
    {
	switch (address) {
	case 0x20:
	case 0x21:
	    if (master.ioPortWrite(address, (byte)data))
		this.updateIRQ();
	    break;
	case 0xa0:
	case 0xa1:
	    if (slave.ioPortWrite(address, (byte)data))
		this.updateIRQ();
	    break;
	case 0x4d0:
	    master.elcrWrite((byte)data);
	    break;
	case 0x4d1:
	    slave.elcrWrite((byte)data);
	    break;
	default:
	}
    }
    public void ioPortWriteWord(int address, int data)
    {
	this.ioPortWriteByte(address, data);
	this.ioPortWriteByte(address + 1, data >> 8);
    }
    public void ioPortWriteLong(int address, int data)
    {
	this.ioPortWriteWord(address, data);
	this.ioPortWriteWord(address + 2, data >> 16);
    }

    /* END IOPortCapable Defined Methods */

    private void masterPollCode()
    {
	master.andInterruptServiceRegister((byte)~(1 << 2));
	master.andInterruptRequestRegister((byte)~(1 << 2));
    }

    private boolean ioportRegistered;

    public void reset()
    {
	master.reset();
	slave.reset();

	ioportRegistered = false;
	connectedCPU = null;
    }
    public boolean initialised()
    {
	return ((connectedCPU != null) && ioportRegistered);
    }

    public void acceptComponent(HardwareComponent component)
    {
	if (component instanceof Processor)
	    connectedCPU = (Processor)component;
	if ((component instanceof IOPortHandler)
	    && component.initialised()) {
	    ((IOPortHandler)component).registerIOPortCapable(this);
	    ioportRegistered = true;
	}
    }

    public String toString()
    {
	return "Intel i8259 Programmable Interrupt Controller";
    }
}

