/*
    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.memory.*;
import org.jpc.emulator.HardwareComponent;

public class DMAController implements IOPortCapable, HardwareComponent
{
    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 CMD_MEMORY_TO_MEMORY = 0x01;
    private static final int CMD_FIXED_ADDRESS = 0x02;
    private static final int CMD_BLOCK_CONTROLLER = 0x04;
    private static final int CMD_COMPRESSED_TIME = 0x08;
    private static final int CMD_CYCLIC_PRIORITY = 0x10;
    private static final int CMD_EXTENDED_WRITE = 0x20;
    private static final int CMD_LOW_DREQ = 0x40;
    private static final int CMD_LOW_DACK = 0x80;
    private static final int CMD_NOT_SUPPORTED = CMD_MEMORY_TO_MEMORY | CMD_FIXED_ADDRESS | CMD_COMPRESSED_TIME | CMD_CYCLIC_PRIORITY | CMD_EXTENDED_WRITE | CMD_LOW_DREQ | CMD_LOW_DACK;

    private int status;
    private int command;
    private int mask;
    private boolean flipFlop;
    private int dShift;
    private int iobase, pageBase, pageHBase;
    private int controllerNumber;
    private PhysicalAddressSpace memory;

    class DMARegister
    {
	public static final int ADDRESS = 0;
	public static final int COUNT = 1;

	public int nowAddress;
	public int nowCount;
	public short baseAddress;
	public short baseCount;

	public int mode;
	public byte page, pageh, dack, eop;
	public DMATransferCapable transferDevice;

	public DMARegister()
	{
	}

	public void reset()
	{
	    transferDevice = null;
	    nowAddress = nowCount = mode = 0;
	    baseAddress = baseCount = 0;
	    page = pageh = dack = eop = 0;
	}
    }
    private DMARegister[] dmaRegs;

    public DMAController(boolean highPageEnable, boolean zeroth)
    {
	ioportRegistered = false;
	this.dShift = zeroth ? 0 : 1;
	this.iobase = zeroth ? 0x00 : 0xc0;
	this.pageBase = zeroth ? 0x80 : 0x88;
	this.pageHBase = highPageEnable ? (zeroth ? 0x480 : 0x488) : -1;
	this.controllerNumber = zeroth ? 0 : 1;
	dmaRegs = new DMARegister[4];
	for (int i = 0; i < 4; i++)
	    dmaRegs[i] = new DMARegister();
	this.reset();
    }

    public boolean isFirst()
    {
	return (this.dShift == 0);
    }

    public void reset()
    {
	for (int i = 0; i < dmaRegs.length; i++)
	    dmaRegs[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;
	DMARegister r = dmaRegs[channelNumber];
	if (getFlipFlop()) {
	    if ((port & 1) == DMARegister.ADDRESS)
		r.baseAddress = (short)((r.baseAddress & 0xff) | ((data << 8) & 0xff00));
	    else
		r.baseCount = (short)((r.baseCount & 0xff) | ((data << 8) & 0xff00));
	    initChannel(channelNumber);
	} else {
	    if ((port & 1) == DMARegister.ADDRESS)
		r.baseAddress = (short)((r.baseAddress & 0xff00) | (data & 0xff));
	    else
		r.baseCount = (short)((r.baseCount & 0xff00) | (data & 0xff));
	}
    }
    private void writeController(int portNumber, int data)
    {
	int port = (portNumber >> this.dShift) & 0x0f;
	switch (port) {
	case 0x08: /* command */
	    if ((data != 0) && ((data & CMD_NOT_SUPPORTED) != 0)) {
		return;
	    }
	    command = data;
	    return;
	case 0x09:
	    int channelNumber = data & 3;
	    if ((data  & 4) != 0) {
		status |= 1 << (channelNumber + 4);
	    } else {
		status &= ~(1 << (channelNumber + 4));
	    }
	    status &= ~(1 << channelNumber);
	    return;
	case 0x0a: /* single mask */
	    if ((data & 0x4) != 0) {
		mask |= 1 << (data & 3);
	    } else {
		mask &= ~(1 << (data & 3));
	    }
	    return;
	case 0x0b: /* mode */
	    channelNumber = data & 3;
	    dmaRegs[channelNumber].mode = data;
	    return;
	case 0x0c: /* clear flipFlop */
	    flipFlop = false;
	    return;
	case 0x0d: /* reset */
	    flipFlop = false;
	    mask = ~0;
	    status = 0;
	    command = 0;
	    return;
	case 0x0e: /* clear mask for all channels */
	    mask = 0;
	    return;
	case 0x0f: /* write mask for all channels */
	    mask = data;
	    return;
	default:
	    return;
	}
    }

    private static final int[] channels = new int[]{-1, 2, 3, 1, 
						      -1, -1, -1, 0};
    private void writePage(int portNumber, int data)
    {
	int channelNumber = channels[portNumber & 7];
	if (-1 == channelNumber) {
	    return;
	}
	dmaRegs[channelNumber].page = (byte)data;
    }
    private void writePageH(int portNumber, int data)
    {
	int channelNumber = channels[portNumber & 7];
	if (-1 == channelNumber) {
	    return;
	}
	dmaRegs[channelNumber].pageh = (byte)data;
    }
    private int readChannel(int portNumber)
    {
	int port = (portNumber >> dShift) & 0x0f;
	int channelNumber = port >> 1;
	int registerNumber = port & 1;
	DMARegister r = dmaRegs[channelNumber];

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

	boolean flipflop = getFlipFlop();
	int val;
	if (registerNumber != 0) {
	    val = ((0xffff & r.baseCount) << dShift)
		- r.nowCount;
	} else {
	    val = r.nowAddress + r.nowCount * direction;
	}
	return (val >>> (dShift + (flipflop ? 0x8 : 0x0))) & 0xff;
    }
    private int readController(int portNumber)
    {
	int val;
	int port = (portNumber >> dShift) & 0x0f;
	switch (port) {
	case 0x08:
	    val = status;
	    status &= 0xf0;
	    break;
	case 0x0f:
	    val = mask;
	    break;
	default:
	    val = 0;
	    break;
	}
	return val;
    }
    private int readPage(int portNumber)
    {
	int channelNumber = channels[portNumber & 7];
	if (-1 == channelNumber) {
	    return 0;
	}
	return 0xff & dmaRegs[channelNumber].page;
    }
    private int readPageH(int portNumber)
    {
	int channelNumber = channels[portNumber & 7];
	if (-1 == channelNumber) {
	    return 0;
	}
	return 0xff & dmaRegs[channelNumber].pageh;
    }
    public void ioPortWriteByte(int address, int data)
    {
	if(dShift == 0) {
	    switch (address - iobase) {
	    case 0:
	    case 1:
	    case 2:
	    case 3:
	    case 4:
	    case 5:
	    case 6:
	    case 7:
		writeChannel(address, data);
		return;
	    default:
		break;
	    }
	    switch (address - iobase) {
	    case 0x8:
	    case 0x9:
	    case 0xa:
	    case 0xb:
	    case 0xc:
	    case 0xd:
	    case 0xe:
	    case 0xf:
		writeController(address, data);
		return;
	    default:
		break;
	    }
	} else {
	    switch (address - iobase) {
	    case 0x0:
	    case 0x2:
	    case 0x4:
	    case 0x6:
	    case 0x8:
	    case 0xa:
	    case 0xc:
	    case 0xe:
		writeChannel(address, data);
		return;
	    default:
		break;
	    }
	    switch (address - iobase) {
	    case 0x10:
	    case 0x12:
	    case 0x14:
	    case 0x16:
	    case 0x18:
	    case 0x1a:
	    case 0x1c:
	    case 0x1e:
		writeController(address, data);
		return;
	    default:
		break;
	    }
	}

	switch (address - pageBase) {
	case pagePortList0:
	case pagePortList1:
	case pagePortList2:
	case pagePortList3:
	    writePage(address, data);
	    return;
	default:
	    break;
	}
	switch (address - pageHBase) {
	case pagePortList0:
	case pagePortList1:
	case pagePortList2:
	case pagePortList3:
	    writePageH(address, data);
	    return;
	default:
	    break;
	}
    }
    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);
    }

    public int ioPortReadByte(int address)
    {
	switch ((address - iobase) >> dShift) {
	case 0:
	case 1:
	case 2:
	case 3:
	case 4:
	case 5:
	case 6:
	case 7:
	    return readChannel(address);
	default:
	    break;
	}
	switch ((address - iobase) >> dShift) {
	case 0x8:
	case 0x9:
	case 0xa:
	case 0xb:
	case 0xc:
	case 0xd:
	case 0xe:
	case 0xf:
	    return readController(address);
	default:
	    break;
	}
	switch (address - pageBase) {
	case pagePortList0:
	case pagePortList1:
	case pagePortList2:
	case pagePortList3:
	    return readPage(address);
	default:
	    break;
	}
	switch (address - pageHBase) {
	case pagePortList0:
	case pagePortList1:
	case pagePortList2:
	case pagePortList3:
	    return readPageH(address);
	default:
	    break;
	}
	return 0xff;
    }
    public int ioPortReadWord(int address)
    {
	return (0xff & this.ioPortReadByte(address)) | (( this.ioPortReadByte(address) << 8) & 0xff);
    }
    public int ioPortReadLong(int address)
    {
	return (0xffff & this.ioPortReadByte(address)) | (( this.ioPortReadByte(address) << 16) & 0xffff);
    }

    public int[] ioPortsRequested()
    {
	int[] temp;
	if (pageHBase >= 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++] = pageBase + pagePortList[i];
	    if (pageHBase >= 0) {
		temp[j++] = pageHBase + 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)
    {
	DMARegister r = dmaRegs[channelNumber];
	r.nowAddress = (0xffff & r.baseAddress) <<dShift;
	r.nowCount = 0;
    }

    public void runTransfers()
    {
	for(int channelNumber = 0; channelNumber < 4; channelNumber++) {
	    int mask = 1 << channelNumber;
	    if ((0 == (this.mask & mask)) && (0 != (this.status & (mask << 4)))) {
		runChannel(channelNumber);
	    }
	}
    }

    private void runChannel(int channelNumber)
    {
	DMARegister r = dmaRegs[channelNumber];
	int n = r.transferDevice.transferHandler(channelNumber + (controllerNumber << 2),
				  r.nowCount,
				  (r.baseCount + 1) << controllerNumber);
	r.nowCount = n;
    }

    public int getChannelMode(int channelNumber)
    {
	return dmaRegs[channelNumber].mode;
    }

    public void holdDREQ(int channelNumber)
    {
	status |= 1 << (channelNumber + 4);
    }

    public void releaseDREQ(int channelNumber)
    {
	status &= ~(1 << (channelNumber + 4));
    }

    public void registerChannel(int channelNumber, DMATransferCapable device)
    {
	dmaRegs[channelNumber].transferDevice = device;
    }

    public int readMemory(int channelNumber, byte[] buffer, int bufferOffset, int position, int length)
    {
	DMARegister r = dmaRegs[channelNumber];

	long address = ((r.pageh & 0x7fl) << 24) |
	    ((0xffl & r.page) << 16) |
	    (0xffffffffl & r.nowAddress);

	if ((r.mode & 0x20) != 0) {
	    System.err.println("DMA Read In Address Decrement Mode!");
	    //This may be broken for 16bit DMA
	    memory.copyContentsInto((int)(address - position - length), buffer, bufferOffset, length);
	    //Should have really decremented address with each byte read, so instead just reverse array order
	    for (int left = bufferOffset, right = bufferOffset + length - 1; left < right; left++, right--) {
		byte temp = buffer[left];
		buffer[left] = buffer[right];
		buffer[right] = temp; // exchange the first and last
	    }
	} else
	    memory.copyContentsInto((int)(address + position), buffer, bufferOffset, length);

	return length;

    }

    public int writeMemory(int channelNumber, byte[] buffer, int bufferOffset, int position, int length)
    {
	DMARegister r = dmaRegs[channelNumber];
	long address = ((0x7fl & r.pageh) << 24) | ((0xffl & r.page) << 16) | (0xffffffffl & r.nowAddress);

	if ((r.mode & 0x20) != 0) {
	    System.err.println("DMA 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 = bufferOffset, right = bufferOffset + length - 1; left < right; left++, right--) {
		byte temp = buffer[left];
		buffer[left] = buffer[right];
		buffer[right] = temp; // exchange the first and last
	    }
	    memory.copyContentsFrom((int)(address - position - length), buffer, bufferOffset, length);
	} else
	    memory.copyContentsFrom((int)(address + position), buffer, bufferOffset, length);

	return length;
    }

    private boolean ioportRegistered;

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

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

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