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

/**
 * i8254 Interval Timer emulation.
 */
public class IntervalTimer implements IOPortCapable, HardwareComponent
{
    private static final int RW_STATE_LSB = 1;
    private static final int RW_STATE_MSB = 2;
    private static final int RW_STATE_WORD0 = 3;
    private static final int RW_STATE_WORD1 = 4;

    private static final int MODE_INTERRUPT_ON_TERMINAL_COUNT = 0;
    private static final int MODE_HARDWARE_RETRIGGERABLE_ONE_SHOT = 1;
    private static final int MODE_RATE_GENERATOR = 2;
    private static final int MODE_SQUARE_WAVE = 3;
    private static final int MODE_SOFTWARE_TRIGGERED_STROBE = 4;
    private static final int MODE_HARDWARE_TRIGGERED_STROBE = 5;

    private static final int PIT_FREQ = 1193182;

    private TimerChannel[] channels;

    private InterruptController irqDevice;
    private Clock timingSource;

    private boolean ioportRegistered;
    private int ioPortBase;
    private int irq;

    static final long scale64(long input, int multiply, int divide)
    {
	//return (BigInteger.valueOf(input).multiply(BigInteger.valueOf(multiply)).divide(BigInteger.valueOf(divide))).longValue();

	long rl = (0xffffffffl & input) * multiply;
	long rh = (input >>> 32) * multiply;
	
	rh += (rl >> 32);
	
	long resultHigh = 0xffffffffl & (rh / divide);
	long resultLow = 0xffffffffl & ((((rh % divide) << 32) + (rl & 0xffffffffl)) / divide);
	
	return (resultHigh << 32) | resultLow;
    }
    
    public IntervalTimer(int ioPort, int irq)
    {
	this.irq = irq;
	ioPortBase = ioPort;
    }

    public int[] ioPortsRequested()
    {
	int temp = ioPortBase;
	return new int[]{temp, temp + 1, temp + 2, temp + 3};
    }
    public int ioPortReadLong(int address)
    {
	return (ioPortReadWord(address) & 0xffff) | 
	    ((ioPortReadWord(address + 2) << 16) & 0xffff0000);
    }
    public int ioPortReadWord(int address)
    {
	return (ioPortReadByte(address) & 0xff) | 
	    ((ioPortReadByte(address + 1) << 8) & 0xff00);
    }
    public int ioPortReadByte(int address)
    {
	return channels[address & 0x3].read();
    }
    public void ioPortWriteLong(int address, int data)
    {
	this.ioPortWriteWord(address, 0xffff & data);
	this.ioPortWriteWord(address + 2, 0xffff & (data >> 16));
    }
    public void ioPortWriteWord(int address, int data)
    {
	this.ioPortWriteByte(address, 0xff & data);
	this.ioPortWriteByte(address + 1, 0xff & (data >> 8));
    }
    public void ioPortWriteByte(int address, int data)
    {
	data &= 0xff;
	address &= 0x3;
	if(address == 3) {
	    int channel = data >> 6;
	    if (channel == 3) {
		/* read back command */
		for(channel = 0; channel < 3; channel++) {
		    if(0 != (data & (2 << channel))) // if channel enabled
			channels[channel].readBack(data);
		}
	    } else {
		channels[channel].writeControlWord(data);
	    }
	} else {
	    channels[address].write(data);
	}
    }

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

    public int getOut(int channel, long currentTime)
    {
	return channels[channel].getOut(currentTime);
    }

    public boolean getGate(int channel)
    {
	return channels[channel].getGate();
    }

    public void setGate(int channel, boolean value)
    {
	channels[channel].setGate(value);
    }

    private InterruptController getInterruptController() { return irqDevice; }   
    private Clock getTimingSource() { return timingSource; }

    class TimerChannel implements TimerResponsive
    {
	private int countValue;

	private int outputLatch;
	private int inputLatch;

	private int countLatched;
	private boolean statusLatched;

	private boolean gate;

	private int status;
	private int readState;
	private int writeState;
	private int rwMode;
	private int mode;
	private int bcd; /* not supported */

	private long countStartTime;
	/* irq handling */
	private long nextTransitionTimeValue;
	private Timer irqTimer;
	private int irq;

	public TimerChannel(int index)
	{
	    reset(index);
	}

	public int read()
	{
	    if (statusLatched) {
		statusLatched = false;
		return status;
	    } 

	    switch (countLatched) {
	    case RW_STATE_LSB:
		countLatched = 0;
		return outputLatch & 0xff;
	    case RW_STATE_MSB:
		countLatched = 0;
		return (outputLatch >> 8) & 0xff;
	    case RW_STATE_WORD0:
		countLatched = RW_STATE_MSB;
		return outputLatch & 0xff;
	    default:
	    case 0: //not latched
		switch(readState) {
		default:
		case RW_STATE_LSB:
		    return getCount() & 0xff;
		case RW_STATE_MSB:
		    return (getCount() >> 8) & 0xff;
		case RW_STATE_WORD0:
		    readState = RW_STATE_WORD1;
		    return getCount() & 0xff;
		case RW_STATE_WORD1:
		    readState = RW_STATE_WORD0;
		    return (getCount() >> 8) & 0xff;
		}
	    }
	}

	public void readBack(int data)
	{
	    if (0 == (data & 0x20)) {
		latchCount();
	    }
	    if ((0 == (data & 0x10)) && !statusLatched) {
		/* status latch */
		/* XXX: add BCD and null count */
		status = 0xff & ((getOut(IntervalTimer.this.getTimingSource().getTime()) << 7) | (rwMode << 4) | (mode << 1) | bcd);
		statusLatched = true;
	    }
	}

	public void write(int data)
	{
	    switch(writeState) {
	    default:
	    case RW_STATE_LSB:
		loadCount(0xff & data);
		break;
	    case RW_STATE_MSB:
		loadCount((0xff & data) << 8);
		break;
	    case RW_STATE_WORD0:
		inputLatch = data;
		writeState = RW_STATE_WORD1;
		break;
	    case RW_STATE_WORD1:
		loadCount((0xff & inputLatch) | ((0xff & data) << 8));
		writeState = RW_STATE_WORD0;
		break;
	    }
	}

	public void writeControlWord(int data)
	{
	    int access = (data >> 4) & 3;

	    if (access == 0) {
		latchCount();
	    } else {
		rwMode = access;
		readState = access;
		writeState = access;
		
		mode = (data >> 1) & 7;
		bcd = (data & 1);
		/* XXX: update irq timer ? */
	    }
	}

	public void setGate(boolean value)
	{
	    switch(mode) {
	    default:
	    case MODE_INTERRUPT_ON_TERMINAL_COUNT:
	    case MODE_SOFTWARE_TRIGGERED_STROBE:
		/* XXX: just disable/enable counting */
		break;
	    case MODE_HARDWARE_RETRIGGERABLE_ONE_SHOT:
	    case MODE_HARDWARE_TRIGGERED_STROBE:
		if (!gate && value) {
		    /* restart counting on rising edge */
		    countStartTime = IntervalTimer.this.getTimingSource().getTime();
		    irqTimerUpdate(countStartTime);
		}
		break;
	    case MODE_RATE_GENERATOR:
	    case MODE_SQUARE_WAVE:
		if (!gate && value) {
		    /* restart counting on rising edge */
		    countStartTime = IntervalTimer.this.getTimingSource().getTime();
		    irqTimerUpdate(countStartTime);
		}
		/* XXX: disable/enable counting */
		break;
	    }
	    this.gate = value;
	}

	private int getCount()
	{
            long d = scale64(IntervalTimer.this.getTimingSource().getTime() - countStartTime, PIT_FREQ, (int)IntervalTimer.this.getTimingSource().getTickRate());

	    switch(mode) {
	    case MODE_INTERRUPT_ON_TERMINAL_COUNT:
	    case MODE_HARDWARE_RETRIGGERABLE_ONE_SHOT:
	    case MODE_SOFTWARE_TRIGGERED_STROBE:
	    case MODE_HARDWARE_TRIGGERED_STROBE:
	        return (int)((countValue - d) & 0xffffl);
	    case MODE_SQUARE_WAVE:
		return (int)(countValue - ((2 * d) % countValue));
	    case MODE_RATE_GENERATOR:
	    default:
		return (int)(countValue - (d % countValue));
	    }
	}
	private int getOut(long currentTime)
	{
	    long d = scale64(currentTime - countStartTime, PIT_FREQ,
			     (int)IntervalTimer.this.getTimingSource().getTickRate());
	    switch(mode) {
	    default:
	    case MODE_INTERRUPT_ON_TERMINAL_COUNT:
		if (d >= countValue)
		    return 1;
		else
		    return 0;
	    case MODE_HARDWARE_RETRIGGERABLE_ONE_SHOT:
		if (d < countValue)
		    return 1;
		else
		    return 0;
	    case MODE_RATE_GENERATOR:
		if (((d % countValue) == 0) && (d != 0)) {
		    return 1;
		} else
		    return 0;
	    case MODE_SQUARE_WAVE:
		if ((d % countValue) < ((countValue + 1) >> 1))
		    return 1;
		else
		    return 0;
	    case MODE_SOFTWARE_TRIGGERED_STROBE:
	    case MODE_HARDWARE_TRIGGERED_STROBE:
		if (d == countValue)
		    return 1;
		else
		    return 0;
	    }
	}

	private long getNextTransitionTime(long currentTime)
	{
	    long nextTime;

            long d = scale64(currentTime - countStartTime, PIT_FREQ,
			     (int)IntervalTimer.this.getTimingSource().getTickRate());
	    switch(mode) {
	    default:
	    case MODE_INTERRUPT_ON_TERMINAL_COUNT:
	    case MODE_HARDWARE_RETRIGGERABLE_ONE_SHOT:
		{
		    if (d < countValue)
			nextTime = countValue;
		    else
			return -1;
		}
		break;
	    case MODE_RATE_GENERATOR:
		{
		    long base = (d / countValue) * countValue;
		    if ((d - base) == 0 && d != 0)
			nextTime = base + countValue;
		    else
			nextTime = base + countValue + 1;
		}
		break;
	    case MODE_SQUARE_WAVE:
		{
		    long base = (d / countValue) * countValue;
		    long period2 = ((countValue + 1) >>> 1);
		    if ((d - base) < period2) 
			nextTime = base + period2;
		    else
			nextTime = base + countValue;
		}
		break;
	    case MODE_SOFTWARE_TRIGGERED_STROBE:
	    case MODE_HARDWARE_TRIGGERED_STROBE:
		{
		    if (d < countValue)
			nextTime = countValue;
		    else if (d == countValue)
			nextTime = countValue + 1;
		    else
			return -1;
		}
		break;
	    }
	    /* convert to timer units */
	    nextTime = countStartTime + scale64(nextTime, (int)IntervalTimer.this.getTimingSource().getTickRate(), PIT_FREQ);

	    /* fix potential rounding problems */
	    /* XXX: better solution: use a clock at PIT_FREQ Hz */
	    if (nextTime <= currentTime)
		nextTime = currentTime + 1;
	    return nextTime;   
	}


	private void loadCount(int value)
	{
	    if (value == 0)
		value = 0x10000;
	    countStartTime = IntervalTimer.this.getTimingSource().getTime();
	    countValue = value;
	    this.irqTimerUpdate(countStartTime);
	}

	private void latchCount()
	{
	    if (0 != countLatched) {
		outputLatch = this.getCount();
		countLatched = rwMode;
	    }
	}

	private void irqTimerUpdate(long currentTime)
	{
	    if (irqTimer == null)
		return;
	    long expireTime = this.getNextTransitionTime(currentTime);
	    int irqLevel = this.getOut(currentTime);
	    IntervalTimer.this.getInterruptController().setIRQ(irq, irqLevel);
	    nextTransitionTimeValue = expireTime;
	    if (expireTime != -1)
		irqTimer.setExpiry(expireTime);
	    else
		irqTimer.setStatus(false);
	}

	public void timerCallback()
	{
	    this.irqTimerUpdate(nextTransitionTimeValue);
	}

	public void reset(int i)
	{    
	    mode = MODE_SQUARE_WAVE;
	    gate = (i != 2);
	    loadCount(0);
	}

	public boolean getGate()
	{ return gate; }

	private void setIRQTimer(Timer object)
	{ irqTimer = object; }

	public void setIRQ(int irq)
	{ this.irq = irq; }
    }

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

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

	if (this.initialised() && (channels == null)) {
	    channels = new TimerChannel[3];
	    for (int i = 0; i < channels.length; i++) {
		channels[i] = new TimerChannel(i);
	    }
	    channels[0].setIRQTimer(timingSource.newTimer(channels[0]));
	    channels[0].setIRQ(irq);
	}
    }

    public String toString()
    {
	return "Intel i8254 Interval Timer";
    }
}

