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

import org.jpc.emulator.motherboard.*;
import org.jpc.emulator.memory.*;
import org.jpc.emulator.memory.codeblock.*;
import org.jpc.emulator.pci.peripheral.*;
import org.jpc.emulator.pci.*;
import org.jpc.emulator.peripheral.*;
import org.jpc.emulator.processor.*;
import org.jpc.support.*;
import java.io.*;
import java.util.*;

/**
 * The main parent class for JPC.
 */
public class PC 
{
    private Processor processor;

    private IOPortHandler ioportHandler;
    private InterruptController irqController;
    private PhysicalAddressSpace physicalAddr;
    private LinearAddressSpace linearAddr;
    private IntervalTimer pit;
    private RTC rtc;
    private DMAController primaryDMA, secondaryDMA;
    private GateA20Handler gateA20;

    private PCIHostBridge pciHostBridge;
    private PCIISABridge pciISABridge;
    private PCIBus pciBus;
    private PIIX3IDEInterface ideInterface;

    private EthernetCard networkCard;
    private VGACard graphicsCard;
    private SerialPort serialDevice0;
    private Keyboard kbdDevice;
    private PCSpeaker speaker;
    private FloppyController fdc;

    private Clock vmClock;
    private DriveSet drives;

    private VGABIOS vgaBIOS;
    private SystemBIOS sysBIOS;

    private HardwareComponent[] myParts;

    private MemoryManager memoryManager;

    public static final int SYS_RAM_SIZE = 256 * 1024 * 1024;

    public PC(Clock clock, DriveSet drives) throws IOException
    {
        this.drives = drives;
	processor = new Processor();
        vmClock = clock;

	//Motherboard
        physicalAddr = new PhysicalAddressSpace();
        for (int i=0; i<SYS_RAM_SIZE; i+= AddressSpace.BLOCK_SIZE)
            //physicalAddr.allocateMemory(i, new ByteArrayMemory(blockSize));
            //physicalAddr.allocateMemory(i, new CompressedByteArrayMemory(blockSize));
            physicalAddr.allocateMemory(i, new LazyMemory(AddressSpace.BLOCK_SIZE));

	memoryManager = MemoryManager.getInstance();
        linearAddr = new LinearAddressSpace();
       	ioportHandler = new IOPortHandler();
	irqController = new InterruptController();
	primaryDMA = new DMAController(false, true);
	secondaryDMA = new DMAController(false, false);
	rtc = new RTC(0x70, 8);
	pit = new IntervalTimer(0x40, 0);
	gateA20 = new GateA20Handler();

	//Peripherals
	ideInterface = new PIIX3IDEInterface();
	networkCard = new EthernetCard();
	graphicsCard = new VGACard();
	serialDevice0 = new SerialPort(0);

	kbdDevice = new Keyboard();
	fdc = new FloppyController();
	speaker = new PCSpeaker();

	//PCI Stuff
	pciHostBridge = new PCIHostBridge();
	pciISABridge = new PCIISABridge();
	pciBus = new PCIBus();

	//BIOSes
	sysBIOS = new SystemBIOS("bios.bin");
	vgaBIOS = new VGABIOS("vgabios.bin");

	myParts = new HardwareComponent[]{processor, vmClock, physicalAddr, linearAddr,
					  ioportHandler, irqController,
					  primaryDMA, secondaryDMA, rtc, pit, gateA20,
					  pciHostBridge, pciISABridge, pciBus,
					  ideInterface, drives, networkCard,
					  graphicsCard, serialDevice0,
					  kbdDevice, fdc, speaker,
					  sysBIOS, vgaBIOS, memoryManager};
	
	if (!configure())
            throw new IllegalStateException("PC Configuration failed");
    }

    public ObjectTreeCache getObjectTreeCache()
    {
	return 	MemoryManager.getInstance().getObjectTreeCache();
    }

    public DriveSet getDrives()
    {
        return drives;
    }

    public BlockDevice getBootDevice()
    {
        return drives.getBootDevice();
    }

    public int getBootType()
    {
        return drives.getBootType();
    }

    private boolean configure()
    {
	boolean fullyInitialised;
	int count = 0;
	do 
        {
	    fullyInitialised = true;
	    for (int j = 0; j < myParts.length; j++) 
            {
		if (myParts[j].initialised() == false) 
                {
		    for (int i = 0; i < myParts.length; i++)
			myParts[j].acceptComponent(myParts[i]);

		    fullyInitialised &= myParts[j].initialised();
		}
	    }
	    count++;
	} 
        while ((fullyInitialised == false) && (count < 100));

	if (count == 100) 
        {
            for (int i=0; i<myParts.length; i++)
                System.out.println("Part "+i+" ("+myParts[i].getClass()+") "+myParts[i].initialised());
	    return false;
        }

	for (int i = 0; i < myParts.length; i++)
        {
	    if (myParts[i] instanceof PCIBus)
		((PCIBus)myParts[i]).biosInit();
	}

	return true;
    }

    public void reset()
    {
	for (int i = 0; i < myParts.length; i++) 
        {
	    if (myParts[i] == this) 
                continue;
	    myParts[i].reset();
	}
        configure();
    }

    public Keyboard getKeyboard()
    {
        return kbdDevice;
    }

    public Processor getProcessor()
    {
        return processor;
    }

    public VGACard getGraphicsCard()
    {
        return graphicsCard;
    }

    public PhysicalAddressSpace getPhysicalMemory()
    {
        return physicalAddr;
    }

    public LinearAddressSpace getLinearMemory()
    {
        return linearAddr;
    }

    public Clock getSystemClock()
    {
        return vmClock;
    }

    public static PC createPC(String[] args, Clock clock) throws IOException
    {
        DriveSet disks = DriveSet.buildFromArgs(args);
        return new PC(clock, disks);
    }

    public final void initRealModeStep()
    {
	vmClock.process();
	primaryDMA.runTransfers();
	secondaryDMA.runTransfers();
    }

    private final int executeRealModeStep(int bulk)
    {
        initRealModeStep();

        int result = 0;
        for (; bulk > 0; bulk--)
        {
            try
            {
                int ip = processor.getInstructionPointer();
                CodeBlock block = physicalAddr.getRealModeCodeBlockAt(ip);
                
                try
                {
                    try
                    {
                        result += block.execute(processor);
                    }
                    catch (NullPointerException e)
                    {
                        block = memoryManager.getRealModeCodeBlockAt(physicalAddr.getReadMemoryBlockAt(ip), ip);
                        result += block.execute(processor);
                    }
                }
                catch (CodeBlockReplacementException e)
                {
                    block = memoryManager.handleCodeBlockReplacement(ip, e);
                    result += block.execute(processor);
                }
                processor.processRealModeInterrupts();
            }
            catch (ProcessorException e) 
            {
		processor.handleRealModeException(e.getVector());
            }
        }

        return result;
    }


    public final CodeBlock decodeBlockAt(int address)
    {
//         if ((processor.getCR0() & 0x1) == 0)
//             return physicalAddr.getRealModeCodeBlockAt(address);
//         else
//             return linearAddr.getProtectedModeCodeBlockAt(address);

        CodeBlock block;
        if ((processor.getCR0() & 0x1) == 0)
        {
            block = physicalAddr.getRealModeCodeBlockAt(address);
            if (block == null)
                block = memoryManager.getRealModeCodeBlockAt(physicalAddr.getReadMemoryBlockAt(address), address);
        }
        else
        {
            block = linearAddr.getProtectedModeCodeBlockAt(address);
            if (block == null)
                block = memoryManager.getProtectedModeCodeBlockAt(linearAddr.getReadMemoryBlockAt(address), address);
        }
        
        return block;
    }

    private final int executeProtectedModeStep(int bulk)
    {    
	vmClock.process();
	primaryDMA.runTransfers();
	secondaryDMA.runTransfers();

        int result = 0;
        for (; bulk > 0; bulk--)
        {
            try 
            {
                int ip = processor.getInstructionPointer();
                CodeBlock block = linearAddr.getProtectedModeCodeBlockAt(ip);
                try
                {
                    try
                    {
                        result += block.execute(processor);
                    }
                    catch (NullPointerException e)
                    {
			Memory memory = linearAddr.getReadMemoryBlockAt(ip);			
			try {
			    block = memoryManager.getProtectedModeCodeBlockAt(memory, ip);
			} catch (NullPointerException f) {
			    memory = linearAddr.validateTLBEntryRead(ip);
			    block = memoryManager.getProtectedModeCodeBlockAt(memory, ip);
			}
			result += block.execute(processor);
                    }
                }
                catch (CodeBlockReplacementException e)
                {
                    block = memoryManager.handleCodeBlockReplacement(ip, e);
                    result += block.execute(processor);
                }

                processor.processProtectedModeInterrupts();
            } 
            catch (ProcessorException e) 
            {
		processor.handleProtectedModeException(e.getVector(), e.hasErrorCode(), e.getErrorCode());
            }
        }

        return result;
    }

    public final int executeRealMode()
    {
	int result = 0;
        int bulk = 10;

        // do it multiple times
        result += executeRealModeStep(bulk);
        result += executeRealModeStep(bulk);
        result += executeRealModeStep(bulk);
        result += executeRealModeStep(bulk);
        result += executeRealModeStep(bulk);
        result += executeRealModeStep(bulk);
        result += executeRealModeStep(bulk);
        result += executeRealModeStep(bulk);
        result += executeRealModeStep(bulk);
        result += executeRealModeStep(bulk);

	return result;
    }
    
    public final int executeProtectedMode()
    {
	int result = 0;
        int bulk = 10;

        // do it multiple times
        result += executeProtectedModeStep(bulk);
        result += executeProtectedModeStep(bulk);
        result += executeProtectedModeStep(bulk);
        result += executeProtectedModeStep(bulk);
        result += executeProtectedModeStep(bulk);
        result += executeProtectedModeStep(bulk);
        result += executeProtectedModeStep(bulk);
        result += executeProtectedModeStep(bulk);
        result += executeProtectedModeStep(bulk);
        result += executeProtectedModeStep(bulk);

	return result;
    }
    
    public final int executeStep()
    {
        try
        {
            if ((processor.getCR0() & 0x1) == 0)
                return executeRealModeStep(1);
            else
                return executeProtectedModeStep(1);
        }
        catch (ModeSwitchException e) 
        {
            return 1;
        }
    }   
}
