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

import org.jpc.emulator.*;
import org.jpc.emulator.motherboard.*;

import java.io.*;
import java.nio.charset.Charset;
import java.util.logging.*;

/**
 * Emulates a standard 16450 UART.
 * 
 * @author Chris Dennis
 */
public class SerialPort extends AbstractHardwareComponent implements IODevice
{
    private static final Logger LOGGING = Logger.getLogger(SerialPort.class.getName());

    private static final Charset US_ASCII = Charset.forName("US-ASCII");
    
    private static final byte UART_LCR_DLAB = (byte)0x80; /* Divisor latch access bit */
    
    private static final byte UART_IER_MSI = 0x08; /* Enable Modem status interrupt */
    private static final byte UART_IER_RLSI = 0x04; /* Enable receiver line status interrupt */
    private static final byte UART_IER_THRI = 0x02; /* Enable Transmitter holding register int. */
    private static final byte UART_IER_RDI = 0x01; /* Enable receiver data interrupt */
    
    private static final byte UART_IIR_NO_INT = 0x01; /* No interrupts pending */
    private static final byte UART_IIR_ID = 0x06; /* Mask for the interrupt ID */
    
    private static final byte UART_IIR_MSI = 0x00; /* Modem status interrupt */
    private static final byte UART_IIR_THRI = 0x02; /* Transmitter holding register empty */
    private static final byte UART_IIR_RDI = 0x04; /* Receiver data interrupt */
    private static final byte UART_IIR_RLSI = 0x06; /* Receiver line status interrupt */

    /*
     * These are the definitions for the Modem Control Register
     */
    private static final byte UART_MCR_LOOP = 0x10; /* Enable loopback test mode */
    private static final byte UART_MCR_OUT2 = 0x08; /* Out2 complement */
    private static final byte UART_MCR_OUT1 = 0x04; /* Out1 complement */
    private static final byte UART_MCR_RTS = 0x02; /* RTS complement */
    private static final byte UART_MCR_DTR = 0x01; /* DTR complement */
    
    /*
     * These are the definitions for the Modem Status Register
     */
    private static final byte UART_MSR_DCD = (byte)0x80; /* Data Carrier Detect */
    private static final byte UART_MSR_RI = 0x40; /* Ring Indicator */
    private static final byte UART_MSR_DSR = 0x20; /* Data Set Ready */
    private static final byte UART_MSR_CTS = 0x10; /* Clear to Send */
    private static final byte UART_MSR_DDCD = 0x08; /* Delta DCD */
    private static final byte UART_MSR_TERI = 0x04; /* Trailing edge ring indicator */
    private static final byte UART_MSR_DDSR = 0x02; /* Delta DSR */
    private static final byte UART_MSR_DCTS = 0x01; /* Delta CTS */
    private static final byte UART_MSR_ANY_DELTA = 0x0F; /* Any of the delta bits! */
    
    private static final byte UART_LSR_TEMT = 0x40; /* Transmitter empty */
    private static final byte UART_LSR_THRE = 0x20; /* Transmit-hold-register empty */
    private static final byte UART_LSR_BI = 0x10; /* Break interrupt indicator */
    private static final byte UART_LSR_FE = 0x08; /* Frame error indicator */
    private static final byte UART_LSR_PE = 0x04; /* Parity error indicator */
    private static final byte UART_LSR_OE = 0x02; /* Overrun error indicator */
    private static final byte UART_LSR_DR = 0x01; /* Receiver data ready */

    private static final int[] ioPorts = new int[]{0x3f8, 0x2f8, 0x3e8, 0x2e8};
    private static final int[] irqLines = new int[]{4, 3, 4, 3};

    private short divider;

    private byte receiverBufferRegister; /* receiver buffer register */

    private byte interruptEnableRegister; /* interrupt enable register */
    private byte interruptIORegister; /* interrupt I/O register */ /* read only */

    private byte lineControlRegister; /* line control register */
    private byte lineStatusRegister; /* line status register */ /* read only */

    private byte modemControlRegister; /* modem control register */
    private byte modemStatusRegister; /* modem status register */

    private byte scratchRegister; /* scratch register */

    private boolean thrIPending; /* transmitter holding register interrupt */
    private int irq; /* irq channel */
    private int baseAddress; /* base I/O Port Pointer */
    private InterruptController irqDevice;

    private final StringBuilder serialOutputBuffer = new StringBuilder();
    private final Logger serialOutput;
    
    public SerialPort(int portNumber)
    {
	ioportRegistered = false;
	if (portNumber > 3 || portNumber < 0) {
            LOGGING.log(Level.WARNING, "port number {0} is not valid, using 0", Integer.valueOf(portNumber));
	    portNumber = 0;
	}
	this.irq = SerialPort.irqLines[portNumber];
	this.baseAddress = SerialPort.ioPorts[portNumber];

	this.lineStatusRegister = UART_LSR_TEMT | UART_LSR_THRE;
	this.interruptIORegister = UART_IIR_NO_INT;
        
        serialOutput = Logger.getLogger(SerialPort.class.getName() + ".port" + portNumber);
    }

    public void saveState(DataOutput output) throws IOException
    {
        output.writeShort(divider);
        output.writeByte(receiverBufferRegister);
        output.writeByte(interruptEnableRegister);
        output.writeByte(interruptIORegister);
        output.writeByte(lineControlRegister);
        output.writeByte(modemControlRegister);
        output.writeByte(lineStatusRegister);
        output.writeByte(modemStatusRegister);
        output.writeByte(scratchRegister);
        output.writeBoolean(thrIPending);
        output.writeInt(irq);
        output.writeInt(baseAddress);
    }

    public void loadState(DataInput input) throws IOException
    {
        ioportRegistered = false;
        divider = input.readShort();
        receiverBufferRegister = input.readByte();
        interruptEnableRegister = input.readByte();
        interruptIORegister = input.readByte();
        lineControlRegister = input.readByte();
        modemControlRegister = input.readByte();
        lineStatusRegister = input.readByte();
        modemStatusRegister = input.readByte();
        scratchRegister = input.readByte();
        thrIPending = input.readBoolean();
        irq = input.readInt();
        baseAddress = input.readInt();
    }

    public int canReceive()
    {
	if (0 == (lineStatusRegister & UART_LSR_DR))
	    return 1;
	else
	    return 0;
    }

    public void recieve(byte data)
    {
	receiverBufferRegister = data;
	if (0 == data)
	    lineStatusRegister = (byte)(lineStatusRegister | UART_LSR_DR);
	else
	    lineStatusRegister = (byte)(lineStatusRegister | UART_LSR_BI | UART_LSR_DR); 
	this.updateIRQ();
    }

    private void updateIRQ()
    {
	if((0 != (lineStatusRegister & UART_LSR_DR)) && (0 != (interruptEnableRegister & UART_IER_RDI))) {
	    interruptIORegister = UART_IIR_RDI;
	} else if (thrIPending && (0 != (interruptEnableRegister & UART_IER_THRI))) {
	    interruptIORegister = UART_IIR_THRI;
	} else {
	    interruptIORegister = UART_IIR_NO_INT;
	}
	if(interruptIORegister != UART_IIR_NO_INT) {
	    irqDevice.setIRQ(irq, 1);
	} else {
	    irqDevice.setIRQ(irq, 0);
	}
    }


    public void ioPortWrite8(int address, int data)
    {
	this.ioportWrite(address, data);
    }
    public void ioPortWrite16(int address, int data){}
    public void ioPortWrite32(int address, int data){}

    public int ioPortRead8(int address)
    {
	return this.ioportRead(address);
    }
    public int ioPortRead16(int address)
    {
	return 0xffff;
    }
    public int ioPortRead32(int address)
    {
	return 0xffffffff;
    }

    public int[] ioPortsRequested()
    {
	return new int[]{baseAddress, baseAddress + 1, baseAddress + 2, baseAddress + 3, baseAddress + 4, baseAddress + 5, baseAddress + 6, baseAddress + 7};
    }

    private void ioportWrite(int address, int data)
    {
	address &= 7;

	switch(address) {
	default:
	case 0:
	    if (0 != (lineControlRegister & UART_LCR_DLAB)) {
		divider = (short)((divider & 0xff00) | data);
	    } else {
		thrIPending = false;
		lineStatusRegister = (byte)(lineStatusRegister & ~UART_LSR_THRE);
		this.updateIRQ();
                print(new String(new byte[]{(byte) data}, US_ASCII));
		//output data
		thrIPending = true;
		lineStatusRegister = (byte)(lineStatusRegister | UART_LSR_THRE | UART_LSR_TEMT);
		this.updateIRQ();
	    }
	    break;
	case 1:
	    if (0 != (lineControlRegister & UART_LCR_DLAB)) {
		divider = (short)((divider & 0x00ff) | (data << 8));
	    } else {
		interruptEnableRegister = (byte)(data & 0x0f);
		if (0 != (lineStatusRegister & UART_LSR_THRE)) {
		    thrIPending = true;
		}
		this.updateIRQ();
	    }
	    break;
	case 2:
	    break;
	case 3:
	    lineControlRegister = (byte)data;
	    break;
	case 4:
	    modemControlRegister = (byte)(data & 0x1f);
	    break;
	case 5:
	    break;
	case 6:
	    modemStatusRegister = (byte)data;
	    break;
	case 7:
	    scratchRegister = (byte)data;
	    break;
	}
    }

    private int ioportRead(int address)
    {
	address &= 7;
	int ret;
	switch(address) {
	default:
	case 0:
	    if(0 != (lineControlRegister & UART_LCR_DLAB)) {
		return divider & 0xff;
	    } else {
		lineStatusRegister = (byte)(lineStatusRegister & ~(UART_LSR_DR | UART_LSR_BI));
		ret = receiverBufferRegister;
		this.updateIRQ();
		return ret;
	    }
	case 1:
	    if (0 != (lineControlRegister & UART_LCR_DLAB)) {
		return (divider >>> 8) & 0xff;
	    } else {
		return interruptEnableRegister;
	    }
	case 2:
	    ret = interruptIORegister;
	    if ((ret & 0x7) == UART_IIR_THRI)
		thrIPending = false;
	    this.updateIRQ();
	    return ret;
	case 3:
	    return lineControlRegister;
	case 4:
	    return modemControlRegister;
	case 5:
	    return lineStatusRegister;
	case 6:
	    if (0 != (modemControlRegister & UART_MCR_LOOP)) {
		/* in loopback, the modem output pins are connected to the inputs */
		ret = (modemControlRegister & 0x0c) << 4;
		ret |= (modemControlRegister & 0x02) << 3;
		ret |= (modemControlRegister & 0x01) << 5;
		return ret;
	    } else {
		return modemStatusRegister;
	    }
	case 7:
	    return scratchRegister;
	}
    }

    private boolean ioportRegistered;

    public void reset()
    {
	irqDevice = null;
	ioportRegistered = false;

	this.lineStatusRegister = UART_LSR_TEMT | UART_LSR_THRE;
	this.interruptIORegister = UART_IIR_NO_INT;
    }

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

    public boolean updated()
    {
	return ioportRegistered && irqDevice.updated();
    }

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

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

    private void print(String data)
    {
        synchronized (serialOutputBuffer) {
            int newline;
            while ((newline = data.indexOf('\n') + 1) > 0) {
                serialOutputBuffer.append(data.substring(0, newline));
                serialOutput.log(Level.INFO, serialOutputBuffer.toString());
                serialOutputBuffer.delete(0, serialOutputBuffer.length());
                data = data.substring(newline);
            }
            serialOutputBuffer.append(data);
        }
    }
}
