/*
    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.memory.*;
import java.io.*;
import java.util.logging.*;

/**
 * Emulation of an 8237 Direct Memory Access Controller
 * @see <a href="http://pdos.csail.mit.edu/6.828/2007/readings/hardware/8237A.pdf">
 * 8237A - Datasheet</a>
 * @author Chris Dennis
 */
public class DMAController extends AbstractHardwareComponent implements IODevice
{
    private static final Logger LOGGING = Logger.getLogger(DMAController.class.getName());

    private static final int pagePortList0 = 0x1;
    private static final int pagePortList1 = 0x2;
    private static final int pagePortList2 = 0x3;
    private static final int pagePortList3 = 0x7;
    private static final int[] pagePortList = new int[]{pagePortList0,
            pagePortList1, pagePortList2, pagePortList3};
    private static final int COMMAND_MEMORY_TO_MEMORY = 0x01;
    private static final int COMMAND_ADDRESS_HOLD = 0x02;
    private static final int COMMAND_CONTROLLER_DISABLE = 0x04;
    private static final int COMMAND_COMPRESSED_TIMING = 0x08;
    private static final int COMMAND_CYCLIC_PRIORITY = 0x10;
    private static final int COMMAND_EXTENDED_WRITE = 0x20;
    private static final int COMMAND_DREQ_SENSE_LOW = 0x40;
    private static final int COMMAND_DACK_SENSE_LOW = 0x80;
    private static final int CMD_NOT_SUPPORTED = COMMAND_MEMORY_TO_MEMORY | COMMAND_ADDRESS_HOLD | COMMAND_COMPRESSED_TIMING | COMMAND_CYCLIC_PRIORITY | COMMAND_EXTENDED_WRITE | COMMAND_DREQ_SENSE_LOW | COMMAND_DACK_SENSE_LOW;
    private static final int ADDRESS_READ_STATUS = 0x8;
    private static final int ADDRESS_READ_MASK = 0xf;
    private static final int ADDRESS_WRITE_COMMAND = 0x8;
    private static final int ADDRESS_WRITE_REQUEST = 0x9;
    private static final int ADDRESS_WRITE_MASK_BIT = 0xa;
    private static final int ADDRESS_WRITE_MODE = 0xb;
    private static final int ADDRESS_WRITE_FLIPFLOP = 0xc;
    private static final int ADDRESS_WRITE_CLEAR = 0xd;
    private static final int ADDRESS_WRITE_CLEAR_MASK = 0xe;
    private static final int ADDRESS_WRITE_MASK = 0xf;
    private int status;
    private int command;
    private int mask;
    private boolean flipFlop;
    private int dShift;
    private int ioBase,  pageLowBase,  pageHighBase;
    private int controllerNumber;
    private PhysicalAddressSpace memory;
    private DMAChannel[] dmaChannels;
    private DMAController slave;

    public class DMAChannel implements Hibernatable
    {
        private static final int MODE_CHANNEL_SELECT = 0x03;
        private static final int MODE_ADDRESS_INCREMENT = 0x20;
        public static final int ADDRESS = 0;
        public static final int COUNT = 1;
        public int currentAddress,  currentWordCount;
        public int baseAddress,  baseWordCount;
        public int mode;
        public int dack,  eop;
        public DMATransferCapable transferDevice;
        public DMAEventHandler eventHandler;
        public int pageLow,  pageHigh;
        private boolean masked = false;

        public void saveState(DataOutput output) throws IOException
        {
            output.writeInt(currentAddress);
            output.writeInt(currentWordCount);
            output.writeInt(baseAddress);
            output.writeInt(baseWordCount);
            output.writeInt(mode);
            output.writeInt(pageLow);
            output.writeInt(pageHigh);
            output.writeInt(dack);
            output.writeInt(eop);
        //tactfully ignore transferDevice

        }

        public void loadState(DataInput input) throws IOException
        {
            currentAddress = input.readInt();
            currentWordCount = input.readInt();
            baseAddress = input.readInt();
            baseWordCount = input.readInt();
            mode = input.readInt();
            pageLow = input.readInt();
            pageHigh = input.readInt();
            dack = input.readInt();
            eop = input.readInt();
        //tactfully ignore transferDevice

        }

        public void registerEventHandler(DMAEventHandler handler)
        {
            eventHandler = handler;
            if (masked)
                eventHandler.handleDMAEvent(DMAEvent.DMA_MASKED);
            else
                eventHandler.handleDMAEvent(DMAEvent.DMA_UNMASKED);
        }

        /**
         * Reads memory from this channel.
         * <p>
         * Allows a <code>DMATransferCapable</code> device to read the section of
         * memory currently pointed to by this channels internal registers.
         * @param buffer byte[] to save data in.
         * @param offset offset into <code>buffer</code>.
         * @param position offset into channel's memory.
         * @param length number of bytes to read.
         */
        public void readMemory(byte[] buffer, int offset, int position, int length)
        {
            int address = (pageHigh << 24) | (pageLow << 16) | currentAddress;

            if ((mode & DMAChannel.MODE_ADDRESS_INCREMENT) != 0) {
                LOGGING.log(Level.WARNING, "read in address decrement mode");
                //This may be broken for 16bit DMA
                memory.copyContentsIntoArray(address - position - length, buffer, offset, length);
                //Should have really decremented address with each byte read, so instead just reverse array order
                for (int left = offset,  right = offset + length - 1; left < right; left++, right--) {
                    byte temp = buffer[left];
                    buffer[left] = buffer[right];
                    buffer[right] = temp; // exchange the first and last
                }
            } else
                memory.copyContentsIntoArray(address + position, buffer, offset, length);
        }

        /**
         * Writes data to this channel.
         * <p>
         * Allows a <code>DMATransferCapable</code> device to write to the section of
         * memory currently pointed to by this channels internal registers.
         * @param buffer byte[] containing data.
         * @param offset offset into <code>buffer</code>.
         * @param position offset into channel's memory.
         * @param length number of bytes to write.
         */
        public void writeMemory(byte[] buffer, int offset, int position, int length)
        {
            int address = (pageHigh << 24) | (pageLow << 16) | currentAddress;

            if ((mode & DMAChannel.MODE_ADDRESS_INCREMENT) != 0) {
                LOGGING.log(Level.WARNING, "write in address decrement mode");
                //This may be broken for 16bit DMA
                //Should really decremented address with each byte write, so instead we reverse the array order now
                for (int left = offset,  right = offset + length - 1; left < right; left++, right--) {
                    byte temp = buffer[left];
                    buffer[left] = buffer[right];
                    buffer[right] = temp; // exchange the first and last
                }
                memory.copyArrayIntoContents(address - position - length, buffer, offset, length);
            } else
                memory.copyArrayIntoContents(address + position, buffer, offset, length);
        }

        private void setMask(boolean mask)
        {
            if (masked == mask)
                return;
            masked = mask;
            if ((masked) && (eventHandler != null))
                eventHandler.handleDMAEvent(DMAEvent.DMA_MASKED);
            if ((!masked) && (eventHandler != null))
                eventHandler.handleDMAEvent(DMAEvent.DMA_UNMASKED);
        }

        private void run()
        {
            int n = transferDevice.handleTransfer(this, currentWordCount, (baseWordCount + 1) << controllerNumber);
            currentWordCount = n;
        }

        public void reset()
        {
            transferDevice = null;
            currentAddress = currentWordCount = mode = 0;
            baseAddress = baseWordCount = 0;
            pageLow = pageHigh = dack = eop = 0;
        }
    }

    /**
     * Constructs a DMA controller (either primary or secondary).  If
     * <code>highPageEnable</code> is true then 32 bit addressing is possible,
     * otherwise the controller is limited to 24 bits.
     * @param highPageEnable <code>true</code> if 32bit addressing required.
     * @param primary <code>true</code> if primary controller.
     */
    public DMAController(boolean highPageEnable, boolean primary)
    {
        ioportRegistered = false;
        dShift =
                primary ? 0 : 1;
        ioBase =
                primary ? 0x00 : 0xc0;
        pageLowBase =
                primary ? 0x80 : 0x88;
        pageHighBase =
                highPageEnable ? (primary ? 0x480 : 0x488) : -1;
        controllerNumber =
                primary ? 0 : 1;
        dmaChannels =
                new DMAChannel[4];
        for (int i = 0; i < 4; i++)
            dmaChannels[i] = new DMAChannel();
        reset();
    }

    public void saveState(DataOutput output) throws IOException
    {
        output.writeInt(status);
        output.writeInt(command);
        output.writeInt(mask);
        output.writeBoolean(flipFlop);
        output.writeInt(dShift);
        output.writeInt(ioBase);
        output.writeInt(pageLowBase);
        output.writeInt(pageHighBase);
        output.writeInt(controllerNumber);
        output.writeInt(dmaChannels.length);
        for (int i = 0; i < dmaChannels.length; i++)
            dmaChannels[i].saveState(output);
    }

    public void loadState(DataInput input) throws IOException
    {
        ioportRegistered = false;
        status =
                input.readInt();
        command =
                input.readInt();
        mask =
                input.readInt();
        flipFlop =
                input.readBoolean();
        dShift =
                input.readInt();
        ioBase =
                input.readInt();
        pageLowBase =
                input.readInt();
        pageHighBase =
                input.readInt();
        controllerNumber =
                input.readInt();
        int len = input.readInt();
        dmaChannels =
                new DMAChannel[len];
        for (int i = 0; i < dmaChannels.length; i++) {
            dmaChannels[i] = new DMAChannel();
            dmaChannels[i].loadState(input);
        }

    }

    public DMAChannel getChannel(int number)
    {
        if (number < 4)
            return dmaChannels[number];
        return slave.getChannel(number-4);
    }

    /**
     * Returns true if this controller is the primary DMA controller.
     * <p>
     * Non-primary or secondary controllers operate by being chained off the
     * primary controller.
     * @return <code>true</code> if this is the primary DMA controller.
     */
    public boolean isPrimary()
    {
        return (this.dShift == 0);
    }

    public void reset()
    {
        for (int i = 0; i < dmaChannels.length; i++)
            dmaChannels[i].reset();

        this.writeController(0x0d << this.dShift, 0);

        memory =
                null;
        ioportRegistered =
                false;
    }

    private void writeChannel(int portNumber, int data)
    {
        int port = (portNumber >>> dShift) & 0x0f;
        int channelNumber = port >>> 1;
        DMAChannel r = dmaChannels[channelNumber];
        if (getFlipFlop()) {
            if ((port & 1) == DMAChannel.ADDRESS)
                r.baseAddress = (r.baseAddress & 0xff) | ((data << 8) & 0xff00);
            else
                r.baseWordCount = (r.baseWordCount & 0xff) | ((data << 8) & 0xff00);
            initChannel(channelNumber);
        } else if ((port & 1) == DMAChannel.ADDRESS)
            r.baseAddress = (r.baseAddress & 0xff00) | (data & 0xff);
        else
            r.baseWordCount = (r.baseWordCount & 0xff00) | (data & 0xff);
    }

    private void writeController(int portNumber, int data)
    {
        int port = (portNumber >>> this.dShift) & 0x0f;
        switch (port) {
            case ADDRESS_WRITE_COMMAND: /* command */
                if ((data != 0) && ((data & CMD_NOT_SUPPORTED) != 0))
                    break;
                command = data;
                break;
            case ADDRESS_WRITE_REQUEST:
                int channelNumber = data & 3;
                if ((data & 4) == 0)
                    status &= ~(1 << (channelNumber + 4));
                else
                    status |= 1 << (channelNumber + 4);

                status &= ~(1 << channelNumber);
                runTransfers();
                break;
            case ADDRESS_WRITE_MASK_BIT:
                if ((data & 0x4) != 0)
                    mask |= 1 << (data & 3);
                else
                {
                    mask &= ~(1 << (data & 3));
                    runTransfers();
                }
                dmaChannels[data & 3].setMask((mask & (1 << (data & 3))) != 0);
                break;
            case ADDRESS_WRITE_MODE:
                channelNumber = data & DMAChannel.MODE_CHANNEL_SELECT;
                dmaChannels[channelNumber].mode = data;
                break;
            case ADDRESS_WRITE_FLIPFLOP:
                flipFlop = false;
                break;
            case ADDRESS_WRITE_CLEAR:
                flipFlop = false;
                mask = ~0;
                status = 0;
                command = 0;
                for (int i=0; i < dmaChannels.length; i++)
                    if (dmaChannels[i] != null)
                    {
                        dmaChannels[i].setMask(true);
                    }
                break;
            case ADDRESS_WRITE_CLEAR_MASK: /* clear mask for all channels */
                mask = 0;
                for (int i=0; i < dmaChannels.length; i++)
                    if (dmaChannels[i] != null)
                    {
                        dmaChannels[i].setMask(false);
                    }
                runTransfers();
                break;
            case ADDRESS_WRITE_MASK: /* write mask for all channels */
                mask = data;
                for (int i=0; i < dmaChannels.length; i++)
                    if (dmaChannels[i] != null)
                    {
                        dmaChannels[i].setMask((mask & (1 << i)) != 0);
                    }
                runTransfers();
                break;
            default:
                break;
        }

    }
    private static final int[] channels = new int[]{-1, 2, 3, 1,
            -1, -1, -1, 0};

    private void writePageLow(int portNumber, int data)
    {
        int channelNumber = channels[portNumber & 7];
        if (-1 == channelNumber)
            return;
        dmaChannels[channelNumber].pageLow = 0xff & data;
    }

    private void writePageHigh(int portNumber, int data)
    {
        int channelNumber = channels[portNumber & 7];
        if (-1 == channelNumber)
            return;
        dmaChannels[channelNumber].pageHigh = 0x7f & data;
    }

    private int readChannel(int portNumber)
    {
        int port = (portNumber >>> dShift) & 0x0f;
        int channelNumber = port >>> 1;
        int registerNumber = port & 1;
        DMAChannel r = dmaChannels[channelNumber];

        int direction = ((r.mode & DMAChannel.MODE_ADDRESS_INCREMENT) == 0) ? 1 : -1;

        boolean flipflop = getFlipFlop();
        int val;
        if (registerNumber != 0)
            val = (r.baseWordCount << dShift) - r.currentWordCount;
        else
            val = r.currentAddress + r.currentWordCount * direction;
        return (val >>> (dShift + (flipflop ? 0x8 : 0x0))) & 0xff;
    }

    private int readController(int portNumber)
    {
        int val;
        int port = (portNumber >>> dShift) & 0x0f;
        switch (port) {
            case ADDRESS_READ_STATUS:
                val = status;
                status &=
                        0xf0;
                break;
            case ADDRESS_READ_MASK:
                val = mask;
                break;
            default:
                val = 0;
                break;
        }

        return val;
    }

    private int readPageLow(int portNumber)
    {
        int channelNumber = channels[portNumber & 7];
        if (-1 == channelNumber)
            return 0;
        return dmaChannels[channelNumber].pageLow;
    }

    private int readPageHigh(int portNumber)
    {
        int channelNumber = channels[portNumber & 7];
        if (-1 == channelNumber)
            return 0;
        return dmaChannels[channelNumber].pageHigh;
    }

    public void ioPortWrite8(int address, int data)
    {
        switch ((address - ioBase) >>> dShift) {
            case 0x0:
            case 0x1:
            case 0x2:
            case 0x3:
            case 0x4:
            case 0x5:
            case 0x6:
            case 0x7:
                writeChannel(address, data);
                return;
            case 0x8:
            case 0x9:
            case 0xa:
            case 0xb:
            case 0xc:
            case 0xd:
            case 0xe:
            case 0xf:
                writeController(address, data);
                return;
            default:
                break;
        }

        switch (address - pageLowBase) {
            case pagePortList0:
            case pagePortList1:
            case pagePortList2:
            case pagePortList3:
                writePageLow(address, data);
                return;
            default:
                break;
        }

        switch (address - pageHighBase) {
            case pagePortList0:
            case pagePortList1:
            case pagePortList2:
            case pagePortList3:
                writePageHigh(address, data);
                return;
            default:
                break;
        }

    }

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

    public int ioPortRead8(int address)
    {
        switch ((address - ioBase) >>> dShift) {
            case 0x0:
            case 0x1:
            case 0x2:
            case 0x3:
            case 0x4:
            case 0x5:
            case 0x6:
            case 0x7:
                return readChannel(address);
            case 0x8:
            case 0x9:
            case 0xa:
            case 0xb:
            case 0xc:
            case 0xd:
            case 0xe:
            case 0xf:
                return readController(address);
            default:
                break;
        }

        switch (address - pageLowBase) {
            case pagePortList0:
            case pagePortList1:
            case pagePortList2:
            case pagePortList3:
                return readPageLow(address);
            default:
                break;
        }

        switch (address - pageHighBase) {
            case pagePortList0:
            case pagePortList1:
            case pagePortList2:
            case pagePortList3:
                return readPageHigh(address);
            default:
                break;
        }

        return 0xff;
    }

    public int ioPortRead16(int address)
    {
        return (0xff & this.ioPortRead8(address)) | ((this.ioPortRead8(address) << 8) & 0xff);
    }

    public int ioPortRead32(int address)
    {
        return (0xffff & this.ioPortRead8(address)) | ((this.ioPortRead8(address) << 16) & 0xffff);
    }

    public int[] ioPortsRequested()
    {
        int[] temp;
        if (pageHighBase >= 0)
            temp = new int[16 + (2 * pagePortList.length)];
        else
            temp = new int[16 + pagePortList.length];

        int j = 0;
        for (int i = 0; i < 8; i++)
            temp[j++] = ioBase + (i << this.dShift);
        for (int i = 0; i < pagePortList.length; i++) {
            temp[j++] = pageLowBase + pagePortList[i];
            if (pageHighBase >= 0)
                temp[j++] = pageHighBase + pagePortList[i];
        }

        for (int i = 0; i < 8; i++)
            temp[j++] = ioBase + ((i + 8) << this.dShift);
        return temp;
    }

    private boolean getFlipFlop()
    {
        boolean ff = flipFlop;
        flipFlop =
                !ff;
        return ff;
    }

    private void initChannel(int channelNumber)
    {
        DMAChannel r = dmaChannels[channelNumber];
        r.currentAddress = r.baseAddress << dShift;
        r.currentWordCount = 0;
    }

    private static int numberOfTrailingZeros(int i) 
    {
	int y;
	if (i == 0) 
            return 32;
	int n = 31;

	y = i << 16; 
        if (y != 0) 
        { 
            n = n - 16; 
            i = y; 
        }

	y = i << 8; 
        if (y != 0) 
        { 
            n = n - 8; 
            i = y; 
        }
	
        y = i << 4; 
        if (y != 0) 
        { 
            n = n - 4; 
            i = y; 
        }
	
        y = i << 2; 
        if (y != 0) 
        { 
            n = n - 2; 
            i = y; 
        }
	
        return n - ((i << 1) >>> 31);
    }
 
    private void runTransfers()
    {
        int value = ~mask & (status >>> 4) & 0xf;
        if (value == 0)
            return;

        while (value != 0) {
            int channel = numberOfTrailingZeros(value);
            if (channel < 4)
                dmaChannels[channel].run();
            else
                break;
            value &= ~(1 << channel);
        }
    }

    /**
     * Returns the mode register of the given DMA channel.
     * @param channel channel index.
     * @return mode register value.
     */
    public int getChannelMode(int channel)
    {
        return dmaChannels[channel].mode;
    }

    /**
     * Request a DMA transfer operation to occur on the specified channel.
     * <p>
     * This is equivalent to pulling the DREQ line high on the controller.
     * @param channel channel index.
     */
    public void holdDmaRequest(int channel)
    {
        status |= 1 << (channel + 4);
        runTransfers();
    }

    /**
     * Request the DMA transfer in operation on the specified channel to stop.
     * <p>
     * This is equivalent to pulling the DREQ line low on the controller.
     * @param channel channel index.
     */
    public void releaseDmaRequest(int channel)
    {
        status &= ~(1 << (channel + 4));
    }

    /**
     * Register the given <code>DMATransferCapable</code> device with the
     * specified channel.
     * <p>
     * Subsequent DMA requests on this channel will call the
     * <code>handleTransfer</code> method on <code>device</code>.
     * @param channel channel index.
     * @param device target of transfers.
     */
    public void registerChannel(int channel, DMATransferCapable device)
    {
        dmaChannels[channel].transferDevice = device;
    }

    private boolean ioportRegistered;

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

    public boolean updated()
    {
        return memory.updated() && ioportRegistered && (!isPrimary() || (slave != null));
    }

    public void acceptComponent(HardwareComponent component)
    {
        if (component instanceof PhysicalAddressSpace)
            this.memory = (PhysicalAddressSpace) component;
        if (component instanceof IOPortHandler) {
            ((IOPortHandler) component).registerIOPortCapable(this);
            ioportRegistered = true;
        }
        if (component instanceof DMAController)
            if (!((DMAController) component).isPrimary())
                slave = (DMAController) component;
    }

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

    }

    public String toString()
    {
        return "DMA Controller [element " + dShift + "]";
    }
}
