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

import org.jpc.emulator.motherboard.IOPortHandler;
import org.jpc.emulator.memory.PhysicalAddressSpace;
import org.jpc.emulator.pci.peripheral.VGACard;
import org.jpc.emulator.*;

import java.io.*;
import java.util.logging.*;

/**
 * Provides an implementation of a PCI bus to allow access to all PCI devices.
 * <p>
 * Currently the PCI bus also performs the auto-configuration of all PCI devices
 * a role which will eventually be taken over by a later version of the system
 * bios.
 * @author Chris Dennis
 */
public class PCIBus extends AbstractHardwareComponent {

    private static final Logger LOGGING = Logger.getLogger(PCIBus.class.getName());
    static final int PCI_DEVICES_MAX = 64;
    static final int PCI_IRQ_WORDS = ((PCI_DEVICES_MAX + 31) / 32);
    private static final byte[] PCI_IRQS = new byte[]{11, 9, 11, 9};
    private int busNumber;
    private int devFNMinimum;
    private boolean updated;
    private int biosIOAddress;
    private int biosMemoryAddress;
    private PCIDevice devices[];
    private PCIISABridge isaBridge;
    private IOPortHandler ioports;
    private PhysicalAddressSpace memory;
    private int pciIRQIndex;
    private int pciIRQLevels[][];

    /**
     * Constructs a default primary PCI bus.
     * <p>
     * This bus can support up to 256 devices, currently only one PCI bus is
     * supported by the JPC emulation.
     */
    public PCIBus() {
        busNumber = 0;
        pciIRQIndex = 0;
        devices = new PCIDevice[256];
        pciIRQLevels = new int[4][PCI_IRQ_WORDS];
        devFNMinimum = 8;
    }

    public void saveState(DataOutput output) throws IOException {
        output.writeInt(busNumber);
        output.writeInt(devFNMinimum);
        output.writeInt(pciIRQIndex);
        output.writeInt(pciIRQLevels.length);
        output.writeInt(pciIRQLevels[0].length);
        for (int[] inner : pciIRQLevels) {
            for (int level : inner) {
                output.writeInt(level);
            }
        }
        System.out.println(biosIOAddress);
        System.out.println(biosMemoryAddress);
        output.writeInt(biosIOAddress);
        output.writeInt(biosMemoryAddress);
    }

    public void loadState(DataInput input) throws IOException {
        updated = false;
        devices = new PCIDevice[256];
        busNumber = input.readInt();
        devFNMinimum = input.readInt();
        pciIRQIndex = input.readInt();
        int len1 = input.readInt();
        int len2 = input.readInt();
        pciIRQLevels = new int[len1][len2];
        for (int i = 0; i < pciIRQLevels.length; i++) {
            for (int j = 0; j < pciIRQLevels[i].length; j++) {
                pciIRQLevels[i][j] = input.readInt();
            }
        }
        biosIOAddress = input.readInt();
        biosMemoryAddress = input.readInt();
    }

    /**
     * Connect a device to this PCI bus.
     * <p>
     * This will trigger the allocation of three basic resources.  Firstly the
     * device is assigned/chooses a device/function number.  Secondly an
     * <code>IRQBouncer</code> is created so that this device can throw interrupts
     * when necessary.  Thirdly the devices io-regions are registered, but not
     * yet allocated addresses.
     * <p>
     * If this bus can support no more devices, or if the device returns an
     * invalid io-region.
     * @param device PCI card to be connected.
     * @return <code>true</code> if the device is successfully registered.
     */
    public boolean registerDevice(PCIDevice device) {
        if (pciIRQIndex >= PCI_DEVICES_MAX) {
            return false;
        }
        if (device.autoAssignDeviceFunctionNumber()) {
            int devFN = findFreeDevFN();
            if (0 <= devFN) {
                device.assignDeviceFunctionNumber(devFN);
            }
        } else {
            PCIDevice oldDevice = devices[device.getDeviceFunctionNumber()];
            if (oldDevice != null) {
                LOGGING.log(Level.INFO, "unregistering pci device {0}", oldDevice);
                oldDevice.deassignDeviceFunctionNumber();
            }
        }

        if (device.getIRQIndex() == -1)
            device.setIRQIndex(pciIRQIndex++);
        this.addDevice(device);

        IRQBouncer bouncer = isaBridge.makeBouncer(device);
        device.addIRQBouncer(bouncer);
        return this.registerPCIIORegions(device);
    }

    private int findFreeDevFN() {
        for (int i = devFNMinimum; i < 256; i += 8) {
            if (null == devices[i]) {
                return i;
            }
        }
        return -1;
    }

    private boolean registerPCIIORegions(PCIDevice device) {
        IORegion[] regions = device.getIORegions();

        if (regions == null) {
            return true;
        }
        boolean ret = true;
        for (IORegion region : regions) {
            if (PCIDevice.PCI_NUM_REGIONS <= region.getRegionNumber()) {
                ret = false;
                continue;
            }
            //region.setAddress(-1);

            if (region.getRegionNumber() == PCIDevice.PCI_ROM_SLOT) {
                device.putConfigLong(PCIDevice.PCI_CONFIG_EXPANSION_ROM_BASE_ADDRESS, region.getType());
            } else {
                device.putConfigLong(PCIDevice.PCI_CONFIG_BASE_ADDRESS + region.getRegionNumber() * 4, region.getType());
            }
        }
        return ret;
    }

    private void updateMappings(PCIDevice device) {
        IORegion[] regions = device.getIORegions();
        if (regions == null) {
            return;
        }
        short command = device.configReadWord(PCIDevice.PCI_CONFIG_COMMAND);
        for (IORegion region : regions) {
            if (null == region) {
                continue;
            }
            if (PCIDevice.PCI_NUM_REGIONS <= region.getRegionNumber()) {
                continue;
            }
            int configOffset;
            if (PCIDevice.PCI_ROM_SLOT == region.getRegionNumber()) {
                configOffset = PCIDevice.PCI_CONFIG_EXPANSION_ROM_BASE_ADDRESS;
            } else {
                configOffset = PCIDevice.PCI_CONFIG_BASE_ADDRESS + region.getRegionNumber() * 4;
            }
            int newAddress = -1;
            if (region instanceof IOPortIORegion) {
                if (0 != (command & PCIDevice.PCI_COMMAND_IO)) {
                    newAddress = device.configReadLong(configOffset);
                    newAddress &= ~(region.getSize() - 1);
                    int lastAddress = newAddress + (int) region.getSize() - 1;

                    if (lastAddress <= (0xffffffffl & newAddress) || 0 == newAddress || 0x10000 <= (0xffffffffl & lastAddress)) {
                        newAddress = -1;
                    }
                }
            } else if (region instanceof MemoryMappedIORegion) {
                if (0 != (command & PCIDevice.PCI_COMMAND_MEMORY)) {
                    newAddress = device.configReadLong(configOffset);
                    if (PCIDevice.PCI_ROM_SLOT == region.getRegionNumber() && (0 == (newAddress & 1))) {
                        newAddress = -1;
                    } else {
                        newAddress &= ~(region.getSize() - 1);
                        int lastAddress = newAddress + (int) region.getSize() - 1;
                        if (lastAddress <= newAddress || 0 == newAddress || -1 == lastAddress) {
                            newAddress = -1;
                        }
                    }
                }
            } else {
                throw new IllegalStateException("Unknown IORegion Type");
            }
            if (region.getAddress() != newAddress) {
                if (region.getAddress() != -1) {
                    if (region instanceof IOPortIORegion) {
                        int deviceClass = device.configReadWord(PCIDevice.PCI_CONFIG_CLASS_DEVICE);
                        if (0x0101 == deviceClass && 4 == region.getSize()) {
                            //r.unmap(); must actually be partial
                            LOGGING.log(Level.WARNING, "supposed to partially unmap");
                            ioports.deregisterIOPortCapable((IOPortIORegion) region);
                        } else //r.unmap();
                        {
                            ioports.deregisterIOPortCapable((IOPortIORegion) region);
                        }
                    } else if (region instanceof MemoryMappedIORegion) {
                        memory.unmap(region.getAddress(), (int) region.getSize());
                    }
                }
                region.setAddress(newAddress);
                if (region.getAddress() != -1) {
                    if (region instanceof IOPortIORegion) {
                        ioports.registerIOPortCapable((IOPortIORegion) region);
                    } else if (region instanceof MemoryMappedIORegion) {
                        memory.mapMemoryRegion((MemoryMappedIORegion) region, region.getAddress(), (int) region.getSize());
                    }
                }
            }
        }
    }

    private void loadMappings(PCIDevice device)
    {
        IORegion[] regions = device.getIORegions();

        for (IORegion region : regions)
            if (region.getAddress() != -1)
            {
                if (region instanceof IOPortIORegion)
                    ioports.registerIOPortCapable((IOPortIORegion) region);
                else if (region instanceof MemoryMappedIORegion)
                    memory.mapMemoryRegion((MemoryMappedIORegion) region, region.getAddress(), (int) region.getSize());
            }
    }

    private void addDevice(PCIDevice device) {
        devices[device.getDeviceFunctionNumber()] = device;
    }

    //PCIHostBridge shifted functionality
    private PCIDevice validPCIDataAccess(int address) {
        int bus = (address >>> 16) & 0xff;
        if (0 != bus) {
            return null;
        }
        return this.devices[(address >>> 8) & 0xff];
    }

    void writePCIDataByte(int address, byte data) {
        PCIDevice device = this.validPCIDataAccess(address);
        if (null == device) {
            return;
        }
        if (device.configWriteByte(address & 0xff, data)) {
            this.updateMappings(device);
        }
    }

    void writePCIDataWord(int address, short data) {
        PCIDevice device = this.validPCIDataAccess(address);
        if (null == device) {
            return;
        }
        if (device.configWriteWord(address & 0xff, data)) {
            this.updateMappings(device);
        }
    }

    void writePCIDataLong(int address, int data) {
        PCIDevice device = this.validPCIDataAccess(address);
        if (null == device) {
            return;
        }
        if (device.configWriteLong(address & 0xff, data)) {
            this.updateMappings(device);
        }
    }

    byte readPCIDataByte(int address) {
        PCIDevice device = this.validPCIDataAccess(address);
        if (null == device) {
            return (byte) 0xff;
        }
        return device.configReadByte(address & 0xff);
    }

    short readPCIDataWord(int address) {
        PCIDevice device = this.validPCIDataAccess(address);
        if (null == device) {
            return (short) 0xffff;
        }
        return device.configReadWord(address & 0xff);
    }

    int readPCIDataLong(int address) {
        PCIDevice device = this.validPCIDataAccess(address);
        if (null == device) {
            return 0xffffffff;
        }
        return device.configReadLong(address & 0xff);
    }

    /**
     * Performs the auto-configuration of PCI devices that is normally handled
     * by the system bios.
     * <p>
     * In later versions this method should be replaced by a more featured BIOS 
     * ROM image, coupled with a more complete PCI emulation.
     */
    public void biosInit() {
        biosIOAddress = 0xc000;
        biosMemoryAddress = 0xf0000000;
        byte elcr[] = new byte[2];

        /* activate IRQ mappings */
        elcr[0] = 0x00;
        elcr[1] = 0x00;
        for (int i = 0; i < 4; i++) {
            byte irq = PCI_IRQS[i];
            /* set to trigger level */
            elcr[irq >> 3] |= (1 << (irq & 7));
            /* activate irq remapping in PIIX */
            isaBridge.configWriteByte(0x60 + i, irq);
        }


        ioports.ioPortWrite8(0x4d0, elcr[0]); // setup io master
        ioports.ioPortWrite8(0x4d1, elcr[1]); // setup io slave

        for (int devFN = 0; devFN < 256; devFN++) {
            PCIDevice device = devices[devFN];
            if (device != null) {
                biosInitDevice(device);
            }
        }
    }

    private final void biosInitDevice(PCIDevice device) {
        int deviceClass = 0xffff & device.configReadWord(PCIDevice.PCI_CONFIG_CLASS_DEVICE);
        int vendorID = 0xffff & device.configReadWord(PCIDevice.PCI_CONFIG_VENDOR_ID);
        int deviceID = 0xffff & device.configReadWord(PCIDevice.PCI_CONFIG_DEVICE_ID);

        switch (deviceClass) {
            case 0x0101:
                if ((0xffff & vendorID) == 0x8086 && (0xffff & deviceID) == 0x7010) {
                    /* PIIX3 IDE */
                    device.configWriteWord(0x40, (short) 0x8000);
                    device.configWriteWord(0x42, (short) 0x8000);
                    defaultIOMap(device);
                } else {
                    /* IDE: we map it as in ISA mode */
                    this.setIORegionAddress(device, 0, 0x1f0);
                    this.setIORegionAddress(device, 1, 0x3f4);
                    this.setIORegionAddress(device, 2, 0x170);
                    this.setIORegionAddress(device, 3, 0x374);
                }
                break;
            case 0x0300:
                if (vendorID == 0x1234) /* VGA: map frame buffer to default Bochs VBE address */ {
                    this.setIORegionAddress(device, 0, 0xe0000000);
                } else {
                    defaultIOMap(device);
                }
                break;
            case 0x0800:
                /* PIC */
                if (vendorID == 0x1014) /* IBM */ {
                    if (deviceID == 0x0046 || deviceID == 0xffff) /* MPIC & MPIC2 */ {
                        this.setIORegionAddress(device, 0, 0x80800000 + 0x00040000);
                    }
                }
                break;
            case 0xff00:
                if (vendorID == 0x0106b && (deviceID == 0x0017 || deviceID == 0x0022)) /* macio bridge */ {
                    this.setIORegionAddress(device, 0, 0x80800000);
                }
                break;
            case 0x200:
                //Ethernet card IO region
                this.setIORegionAddress(device, 0, device.configReadLong(0x10));
                break;
            default:
                defaultIOMap(device);
                break;
        }

        /* map the interrupt */
        int pin = device.configReadByte(PCIDevice.PCI_CONFIG_INTERRUPT_PIN);
        if (pin != 0) {
            pin = isaBridge.slotGetPIRQ(device, pin - 1);
            device.configWriteByte(PCIDevice.PCI_CONFIG_INTERRUPT_LINE, PCI_IRQS[pin]);
        }
    }

    private void defaultIOMap(PCIDevice device) {
        IORegion[] regions = device.getIORegions();
        if (regions == null) {
            return;
        }
        for (IORegion region : regions) {
            if (region == null) {
                continue;
            }
            if (region instanceof IOPortIORegion) {
                int paddr = biosIOAddress;
                paddr = (int) ((paddr + region.getSize() - 1) &
                        ~(region.getSize() - 1));
                this.setIORegionAddress(device, region.getRegionNumber(), paddr);
                biosIOAddress += region.getSize();
            } else if (region instanceof MemoryMappedIORegion) {
                int paddr = biosMemoryAddress;
                paddr = (int) ((paddr + region.getSize() - 1) &
                        ~(region.getSize() - 1));
                this.setIORegionAddress(device, region.getRegionNumber(), paddr);
                biosMemoryAddress += region.getSize();
            }
        }
    }

    private void setIORegionAddress(PCIDevice device, int regionNumber, int address) {
        int offset;
        if (regionNumber == PCIDevice.PCI_ROM_SLOT) {
            offset = PCIDevice.PCI_CONFIG_EXPANSION_ROM_BASE_ADDRESS;
        } else {
            offset = PCIDevice.PCI_CONFIG_BASE_ADDRESS + regionNumber * 4;
        }
        if (device.configWriteLong(offset, address)) {
            this.updateMappings(device);

        /* enable memory mappings */
        }
        IORegion region = device.getIORegion(regionNumber);
        if (region == null) {
            return;
        }
        short command = device.configReadWord(PCIDevice.PCI_CONFIG_COMMAND);
        if (region.getRegionNumber() == PCIDevice.PCI_ROM_SLOT) {
            command |= PCIDevice.PCI_COMMAND_MEMORY;
        } else if (region instanceof IOPortIORegion) {
            command |= PCIDevice.PCI_COMMAND_IO;
        } else {
            command |= PCIDevice.PCI_COMMAND_MEMORY;
        }
        if (device.configWriteWord(PCIDevice.PCI_CONFIG_COMMAND, command)) {
            this.updateMappings(device);
        }
    }

    public void reset() {
        isaBridge = null;
        ioports = null;
        memory = null;

        pciIRQIndex = 0;
        devices = new PCIDevice[256];
        pciIRQLevels = new int[4][PCI_IRQ_WORDS];
    }

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

    public void acceptComponent(HardwareComponent component) {
        if (component instanceof PCIISABridge) {
            isaBridge = (PCIISABridge) component;
        }
        if ((component instanceof IOPortHandler) && component.initialised()) {
            ioports = (IOPortHandler) component;
        }
        if ((component instanceof PhysicalAddressSpace) && component.initialised()) {
            memory = (PhysicalAddressSpace) component;
        //The following call may be unnecessary
        }
        if ((component instanceof VGACard) && (memory != null)) {
            updateMappings((VGACard) component);
        }
    }

    public boolean updated() {
        return updated;
    }

    public void updateComponent(HardwareComponent component) {
        if ((component instanceof VGACard) && component.updated()) {
            updated = true;
        }
    }
}

