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

    A project from the Physics Dept, The University of Oxford

    Copyright (C) 2007-2010 The University of Oxford

    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: 

    jpc.sourceforge.net
    or the developer website
    sourceforge.net/projects/jpc/

    Conceived and Developed by:
    Rhys Newman, Ian Preston, Chris Dennis

    End of licence header
*/

package org.jpc.emulator.motherboard;

import org.jpc.emulator.*;
import org.jpc.emulator.processor.Processor;
import org.jpc.j2se.*;

import java.io.*;
import java.util.logging.*;

/**
 * i8259 Programmable Interrupt Controller emulation.
 */
/**
 * Emulation of an 8259 Programmable Interrupt Controller.
 * @see <a href="http://www.ee.hacettepe.edu.tr/~alkar/ELE414/8259.pdf">82C59A - Datasheet</a>
 * @author Chris Dennis
 */
public class InterruptController extends AbstractHardwareComponent implements IODevice
{
    private static final Logger LOGGING = Logger.getLogger(InterruptController.class.getName());

    private InterruptControllerElement master;
    private InterruptControllerElement slave;

    private Processor connectedCPU;

    /**
     * Constructs a <code>InterruptController</code> which will attach itself to
     * a <code>Processor</code> instance during the configuration stage.
     */
    public InterruptController()
    {
        ioportRegistered = false;
        master = new InterruptControllerElement(true);
        slave = new InterruptControllerElement(false);
    }

    public void saveState(DataOutput output) throws IOException
    {
        master.saveState(output);
        slave.saveState(output);
    }

    public void loadState(DataInput input) throws IOException
    {
        ioportRegistered = false;
        master.loadState(input);
        slave.loadState(input);
    }

    public int getIRQ0Vector()
    {
        return master.irqBase;
    }

    public int getSpuriousVector()
    {
        return slave.irqBase + 7;
    }

    public int getSpuriousMasterVector()
    {
        return master.irqBase + 7;
    }

    public void triggerSpuriousInterrupt()
    {
        setIRQ(2, 0);
        setIRQ(2, 1);
    }

    public void triggerSpuriousMasterInterrupt()
    {
        connectedCPU.raiseInterrupt();
    }

    public int getMasterIRR()
    {
        return master.interruptRequestRegister;
    }

    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();
        }
    }

    /**
     * Set interrupt number <code>irqNumber</code> to level <code>level</code>.
     * @param irqNumber interrupt channel number.
     * @param level requested level.
     */
    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:
	}
    }

    /**
     * Return the highest priority interrupt request currently awaiting service
     * on this interrupt controller.  This is called by the processor emulation
     * once its <code>raiseInterrupt</code> method has been called, to get the
     * correct interrupt vector value.
     * @return highest priority interrupt vector.
     */
    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 slave.irqBase + slaveIRQ;
                //masterIRQ = slaveIRQ + 8;
            } else {
                this.updateIRQ();
                return master.irqBase + masterIRQ;
            }
        } else {
            /* spurious IRQ on host controller */
            masterIRQ = 7;
            this.updateIRQ();
            return master.irqBase + masterIRQ;
        }
    }

    private class InterruptControllerElement implements Hibernatable
    {
        private int lastInterruptRequestRegister; //edge detection
        private int interruptRequestRegister;
        private int interruptMaskRegister;
        private int interruptServiceRegister;

        private int priorityAdd; // highest IRQ priority
        private int irqBase;
        private boolean readRegisterSelect;
        private boolean poll;
        private boolean specialMask;
        private int initState;
        private boolean fourByteInit;
        private int elcr; //(elcr) PIIX3 edge/level trigger selection
        private int elcrMask;

        private boolean specialFullyNestedMode;

        private boolean autoEOI;
        private boolean rotateOnAutoEOI;

        private int[] ioPorts;

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

        public void saveState(DataOutput output) throws IOException
        {
            output.writeInt(lastInterruptRequestRegister);
            output.writeInt(interruptRequestRegister);
            output.writeInt(interruptMaskRegister);
            output.writeInt(interruptServiceRegister);
            output.writeInt(priorityAdd);
            output.writeInt(irqBase);
            output.writeBoolean(readRegisterSelect);
            output.writeBoolean(poll);
            output.writeBoolean(specialMask);
            output.writeInt(initState);
            output.writeBoolean(autoEOI);
            output.writeBoolean(rotateOnAutoEOI);
            output.writeBoolean(specialFullyNestedMode);
            output.writeBoolean(fourByteInit);
            output.writeInt(elcr);
            output.writeInt(elcrMask);
            output.writeInt(ioPorts.length);
            for (int port : ioPorts)
                output.writeInt(port);
        }

        public void loadState(DataInput input) throws IOException
        {
            lastInterruptRequestRegister = input.readInt();
            interruptRequestRegister = input.readInt();
            interruptMaskRegister = input.readInt();
            interruptServiceRegister = input.readInt();
            priorityAdd = input.readInt();
            irqBase = input.readInt();
            readRegisterSelect = input.readBoolean();
            poll = input.readBoolean();
            specialMask = input.readBoolean();
            initState = input.readInt();
            autoEOI = input.readBoolean();
            rotateOnAutoEOI = input.readBoolean();
            specialFullyNestedMode = input.readBoolean();
            fourByteInit = input.readBoolean();
            elcr = input.readInt();
            elcrMask = input.readInt();
            int len = input.readInt();
            ioPorts = new int[len];
            for (int i=0; i< len; i++)
                ioPorts[i] = input.readInt();
        }

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

        public int ioPortRead(int address)
        {
            if(poll) {
                poll = false;
                return this.pollRead(address);
            }
	    
            if ((address & 1) == 0) {
                if (readRegisterSelect) {
                    return interruptServiceRegister;
                }

                return interruptRequestRegister;
            }

            return interruptMaskRegister;
        }

        public int elcrRead()
        {
            return elcr;
        }

        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();

                    initState = 1;
                    fourByteInit = ((data & 1) != 0);
                    if (0 != (data & 0x02))
                        LOGGING.log(Level.INFO, "single mode not supported");
                    if (0 != (data & 0x08))
                        LOGGING.log(Level.INFO, "level sensitive irq not supported");
                }
                else if (0 != (data & 0x08))
                {
                    if (0 != (data & 0x04))
                        poll = true;
                    if (0 != (data & 0x02))
                        readRegisterSelect = ((data & 0x01) != 0);
                    if (0 != (data & 0x40))
                        specialMask = (((data >>> 5) & 1) != 0);
                }
                else
                {
                    command = data >>> 5;
                    switch(command) {
                        case 0:
                        case 4:
                            rotateOnAutoEOI = ((command >>> 2) != 0);
                            break;
                        case 1: // end of interrupt
                        case 5:
                            priority = this.getPriority(interruptServiceRegister);
                            if (priority != 8) {
                                irq = (priority + priorityAdd) & 7;
                                interruptServiceRegister &= ~(1 << irq);
                                if (command == 5)
                                    priorityAdd = (irq + 1) & 7;
                                return true;
                            }
                            break;
                        case 3:
                            irq = data & 7;
                            interruptServiceRegister &= ~(1 << irq);
                            return true;
                        case 6:
                            priorityAdd = (data + 1) & 7;
                            return true;
                        case 7:
                            irq = data & 7;
                            interruptServiceRegister &= ~(1 << irq);
                            priorityAdd = (irq + 1) & 7;
                            return true;
                        default:
                            /* no operation */
                            break;
                    }
                }
            }
            else
            {
                switch(initState)
                {
                    case 0:
                        /* normal mode */
                        interruptMaskRegister = data;
                        return true;
                    case 1:
                        irqBase = data & 0xf8;
                        initState = 2;
                        break;
                    case 2:
                        if (fourByteInit) {
                            initState = 3;
                        } else {
                            initState = 0;
                        }
                        break;
                    case 3:
                        specialFullyNestedMode = (((data >>> 4) & 1) != 0);
                        autoEOI = (((data >>> 1) & 1) != 0);
                        initState = 0;
                        break;
                }
            }
            return false;
        }

        public void elcrWrite(int data)
        {
            elcr = data & elcrMask;
        }
        /* END IODevice 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();
            }
            interruptRequestRegister &= ~(1 << ret);
            interruptServiceRegister &= ~(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 != (elcr & mask)) {
                /* level triggered */
                if (0 != level) {
                    interruptRequestRegister |= mask;
                    lastInterruptRequestRegister |= mask;
                } else {
                    interruptRequestRegister &= ~mask;
                    lastInterruptRequestRegister &= ~mask;
                }
            } else {
                /* edge triggered */
                if (0 != level) {
                    if ((lastInterruptRequestRegister & mask) == 0) {
                        interruptRequestRegister |= mask;
                    }
                    lastInterruptRequestRegister |= mask;
                } else {
                    lastInterruptRequestRegister &= ~mask;
                }
            }
        }

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

        public int getIRQ()
        {
            int mask, currentPriority, priority;
	    
            mask = interruptRequestRegister & ~interruptMaskRegister;
            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 = interruptServiceRegister;
            if (specialFullyNestedMode && this.isMaster()) {
                mask &= ~(1 << 2);
            }
            currentPriority = this.getPriority(mask);

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

        private void intAck(int irqNumber)
        {
            if (autoEOI) {
                if (rotateOnAutoEOI)
                    priorityAdd = (irqNumber + 1) & 7;
            } else {
                interruptServiceRegister |= (1 << irqNumber);
            }
            /* We don't clear a level sensitive interrupt here */
            if (0 == (elcr & (1 << irqNumber)))
                interruptRequestRegister &= ~(1 << irqNumber);
        }

        private boolean isMaster()
        {
            return InterruptController.this.master == this;
        }
        
        private void reset()
        {
            //zero all variables except elcrMask
            lastInterruptRequestRegister = 0x0;
            interruptRequestRegister = 0x0;
            interruptMaskRegister = 0x0;
            interruptServiceRegister = 0x0;
	    
            priorityAdd = 0;
            irqBase = 0x0;
            readRegisterSelect = false;
            poll = false;
            specialMask = false;
            autoEOI = false;
            rotateOnAutoEOI = false;

            specialFullyNestedMode = false;

            initState = 0;
            fourByteInit = false;

            elcr = 0x0; //(elcr) PIIX3 edge/level trigger selection
        }

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


    /* BEGIN IODevice 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 ioPortRead8(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 ioPortRead16(int address)
    {
        return (0xff & ioPortRead8(address)) |
                (0xff00 & (ioPortRead8(address + 1) << 8));
    }
    public int ioPortRead32(int address)
    {
        return (0xffff & ioPortRead16(address)) |
                (0xffff0000 & (ioPortRead16(address + 2) << 16));
    }

    public void ioPortWrite8(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(data);
                break;
            case 0x4d1:
                slave.elcrWrite(data);
                break;
            default:
        }
    }
    public void ioPortWrite16(int address, int data)
    {
        this.ioPortWrite8(address, data);
        this.ioPortWrite8(address + 1, data >>> 8);
    }
    public void ioPortWrite32(int address, int data)
    {
        this.ioPortWrite16(address, data);
        this.ioPortWrite16(address + 2, data >>> 16);
    }

    /* END IODevice Defined Methods */

    private void masterPollCode()
    {
        master.interruptServiceRegister &= ~(1 << 2);
        master.interruptRequestRegister &= ~(1 << 2);
    }

    private boolean ioportRegistered;

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

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

    public void updateComponent(HardwareComponent component)
    {
        if (component instanceof IOPortHandler)
        {
            ((IOPortHandler)component).registerIOPortCapable(this);
            ioportRegistered = true;
        }
    }

    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";
    }
}