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

//Do not even think about adding an import line to this class - especially not import java.net.*!
import java.io.IOException;
import java.io.DataOutput;
import java.io.DataInput;
import java.util.logging.*;

import org.jpc.emulator.AbstractHardwareComponent;

/**
 * Represents the set of disk drive devices associated with this emulator
 * instance.
 * @author Chris Dennis
 */
public class DriveSet extends AbstractHardwareComponent
{
    private static final Logger LOGGING = Logger.getLogger(DriveSet.class.getName());
    
    public static enum BootType {FLOPPY, HARD_DRIVE, CDROM;}

    private static enum Devices {
        DEFAULT("org.jpc.support.FileBackedSeekableIODevice"),
        
        dir("org.jpc.support.TreeBlockDevice"),
        mem("org.jpc.support.ArrayBackedSeekableIODevice"),
        net("org.jpc.support.RemoteSeekableIODevice");

        private final String clazzname;
        
        Devices(String clazz)
        {
            clazzname = clazz;
        }
        
        public Object getInstance()
        {
            try {
                Class clazz = Class.forName(clazzname);
                return clazz.newInstance();
            } catch (ClassNotFoundException e) {
                LOGGING.log(Level.WARNING, "Drive device class not found", e);
                return null;
            } catch (InstantiationException e) {
                LOGGING.log(Level.WARNING, "Drive device couldn't be instantiated", e);
                return null;
            } catch (IllegalAccessException e) {
                LOGGING.log(Level.WARNING, "Drive device couldn't be instantiated", e);
                return null;
            }
        }        
    }
    
    private BootType bootType;
    private BlockDevice[] floppies;
    private BlockDevice[] ides;
    private String[] initialArgs;

    /**
     * Constructs a driveset with one hard disk, one floppy disk and the
     * specified boot device type.
     * @param boot boot device
     * @param floppyDrive first floppy device
     * @param hardDrive primary master hard disk device
     */
    public DriveSet(BootType boot, BlockDevice floppyDrive, BlockDevice hardDrive)
    {
        this(boot, floppyDrive, null, hardDrive, null, null, null);
    }

    /**
     * Constructs a driveset with all parameters specified.
     * <p>
     * A drive set can be composed of at most four ide devices and two floppy
     * drive devices.
     * @param boot boot device
     * @param floppyDriveA first floppy device
     * @param floppyDriveB second floppy device
     * @param hardDriveA primary master hard disk
     * @param hardDriveB primary slave hard disk
     * @param hardDriveC secondary master hard disk
     * @param hardDriveD secondary slave hard disk
     */
    public DriveSet(BootType boot, BlockDevice floppyDriveA, BlockDevice floppyDriveB, BlockDevice hardDriveA, BlockDevice hardDriveB, BlockDevice hardDriveC, BlockDevice hardDriveD)
    {
        this.bootType = boot;

        floppies = new BlockDevice[2];
        floppies[0] = floppyDriveA;
        floppies[1] = floppyDriveB;

        ides = new BlockDevice[4];
        ides[0] = hardDriveA;
        ides[1] = hardDriveB;
        ides[2] = (hardDriveC == null) ? new CDROMBlockDevice() : hardDriveC;
        ides[3] = hardDriveD;
    }

    private void setInitialArgs(String[] init)
    {
        initialArgs = init;
    }

    /**
     * Returns the i'th hard drive device.
     * <p>
     * Devices are numbered from 0 to 3 inclusive in order: primary master,
     * primary slave, secondary master, secondary slave.
     * @param index drive index
     * @return hard drive block device
     */
    public BlockDevice getHardDrive(int index)
    {
        return ides[index];
    }

    public void setHardDrive(int index, BlockDevice device)
    {
        ides[index] = device;
    }

    /**
     * Returns the i'th floppy drive device.
     * <p>
     * The drives are numbered sequentially A:, B:.
     * @param index floppy drive index
     * @return floppy drive block device
     */
    public BlockDevice getFloppyDrive(int index)
    {
        return floppies[index];
    }

    /**
     * Returns the current boot device as determined by the boot type parameter.
     * @return boot block device
     */
    public BlockDevice getBootDevice()
    {
        switch (bootType) {
            case FLOPPY:
                return floppies[0];
            case CDROM:
                return ides[2];
            case HARD_DRIVE:
                return ides[0];
            default:
                return null;
        }
    }

    /**
     * Returns the boot type being used by this driveset.
     * @return boot type
     */
    public BootType getBootType()
    {
        return bootType;
    }

    private static Object createDevice(String spec)
    {
        if (spec == null) {
            return null;
        }

	if ((spec.indexOf("\"") == 0) && (spec.indexOf("\"", 1) > 0))
	    spec = spec.substring(1, spec.length()-2);

	

        int colon = spec.indexOf(':');
        String deviceKey = "DEFAULT";
	String deviceSpec = spec;
        if ((colon >= 0) && (spec.indexOf("\\") != colon + 1)) {
            deviceKey = spec.substring(0, colon);
	    deviceSpec = spec.substring(colon + 1);
        }

        Object device;
        if (deviceKey.startsWith("caching")) {
            deviceKey = "DEFAULT";
            int secondcolon = deviceSpec.indexOf(':');
            if ((secondcolon > 0) && (deviceSpec.indexOf("\\") != secondcolon + 1)) {
                deviceSpec = deviceSpec.substring(secondcolon + 1);
                deviceKey = deviceSpec.substring(0, secondcolon);
            }
            device = Devices.valueOf(deviceKey).getInstance();
            device = new CachingSeekableIODevice((SeekableIODevice) device);
        } else
            device = Devices.valueOf(deviceKey).getInstance();

        if (device instanceof SeekableIODevice) {
            try {
                ((SeekableIODevice) device).configure(deviceSpec);
            } catch (IOException e) {
                return null;
            }
            return device;
        } else if (device instanceof BlockDevice) {
            try {
                ((BlockDevice) device).configure(deviceSpec);
            } catch (IOException e) {
                return null;
            }
            return device;
        } else {
            return device;
        }
    }

    private static BlockDevice createFloppyBlockDevice(String spec)
    {
        Object device = createDevice(spec);

        if (device instanceof SeekableIODevice)
            return new FloppyBlockDevice((SeekableIODevice) device);
        else
            return (BlockDevice) device;
    }

    private static BlockDevice createHardDiskBlockDevice(String spec)
    {
        Object device = createDevice(spec);

        if (device instanceof SeekableIODevice)
            return new HDBlockDevice((SeekableIODevice) device);
        else
            return (BlockDevice) device;
    }

    private static BlockDevice createCdRomBlockDevice(String spec)
    {
        Object device = createDevice(spec);

        if (device instanceof SeekableIODevice)
            return new CDROMBlockDevice((SeekableIODevice) device);
        else
            return (BlockDevice) device;
    }

    public void saveState(DataOutput output) throws IOException
    {
        output.writeInt(initialArgs.length);
        for (int i = 0; i < initialArgs.length; i++)
            output.writeUTF(initialArgs[i]);
    }

    public void loadState(DataInput input) throws IOException
    {
        int len = input.readInt();
        String[] newArgs = new String[len];
        for (int i = 0; i < len; i++)
            newArgs[i] = input.readUTF();
        initialArgs = newArgs;

        //do not load drives
        DriveSet temp = buildFromArgs(initialArgs);

        floppies = new BlockDevice[2];
        for (int i = 0; i < floppies.length; i++)
            floppies[i] = temp.getFloppyDrive(i);

        for (int i = 0; i < ides.length; i++)
            ides[i] = temp.getHardDrive(i);
    }

    /**
     * Constructs a driveset instance by parsing the given command line
     * arguments.
     * @param args command line argument array
     * @return resultant <code>DriveSet</code>
     */
    public static DriveSet buildFromArgs(String[] args)
    {
        String[] initialArgs = args.clone();

        BlockDevice floppyA = createFloppyBlockDevice(ArgProcessor.findVariable(args, "-fda", null));
        BlockDevice floppyB = createFloppyBlockDevice(ArgProcessor.findVariable(args, "-fdb", null));

        BlockDevice hardDiskA = createHardDiskBlockDevice(ArgProcessor.findVariable(args, "-hda", null));
        BlockDevice hardDiskB = createHardDiskBlockDevice(ArgProcessor.findVariable(args, "-hdb", null));
        BlockDevice hardDiskC = createHardDiskBlockDevice(ArgProcessor.findVariable(args, "-hdc", null));
        BlockDevice hardDiskD = createHardDiskBlockDevice(ArgProcessor.findVariable(args, "-hdd", null));

        String cdromSpec = ArgProcessor.findVariable(args, "-cdrom", null);
        if (cdromSpec != null)
            hardDiskC = createCdRomBlockDevice(cdromSpec);

        BootType boot = BootType.FLOPPY;
        String bootArg = ArgProcessor.findVariable(args, "-boot", null);
        if ("fda".equalsIgnoreCase(bootArg))
            boot = BootType.FLOPPY;
        else if ("hda".equalsIgnoreCase(bootArg))
            boot = BootType.HARD_DRIVE;
        else if ("cdrom".equalsIgnoreCase(bootArg))
            boot = BootType.CDROM;
        else if (hardDiskA != null)
            boot = BootType.HARD_DRIVE;
        else if (hardDiskC instanceof CDROMBlockDevice)
            boot = BootType.CDROM;

        DriveSet temp = new DriveSet(boot, floppyA, floppyB, hardDiskA, hardDiskB, hardDiskC, hardDiskD);
        temp.setInitialArgs(initialArgs);
        return temp;
    }

    public void close()
    {
        for (BlockDevice d: ides)
            if (d != null)
                d.close();
        for (BlockDevice d: floppies)
            if (d != null)
                d.close();
    }
}
