package at.ac.tuwien.lsdc.types;

import java.util.HashMap;
import java.util.Iterator;

import at.ac.tuwien.lsdc.exception.ActiveApplicationsException;
import at.ac.tuwien.lsdc.exception.VMResizeException;
import at.ac.tuwien.lsdc.exception.VMsRunningException;
import at.ac.tuwien.lsdc.types.VirtualMachine.VMType;

public class PhysicalMachine implements Comparable<PhysicalMachine> {

	final int BEFORE = -1;
	final int EQUAL = 0;
	final int AFTER = 1;

	private static int count = 0;

	private HashMap<Integer, VirtualMachine> VMs = new HashMap<Integer, VirtualMachine>();

	private int id;

	private final int maxSize = 50000;
	private final int maxRAM = 4700;
	private final int maxCPU = 2400;

	private int reservedSize;
	private int reservedRAM;
	private int reservedCPU;

	private final int initialSize = 850;
	private final int initialRAM = 300;
	private final int initialCPU = 500;

	private int totalVMs = 0;
	private int totalResizeCalls = 0;

	private boolean running = false;

	public PhysicalMachine() {
		id = count;
		count++;
	}

	public void start() {
		reservedSize = initialSize;
		reservedRAM = initialRAM;
		reservedCPU = initialCPU;
		running = true;
	}

	public void stop() throws VMsRunningException {
		if (VMs.size() > 0)
			throw new VMsRunningException("PM cannot be stopped. Some VMs still running");

		VMs = new HashMap<Integer, VirtualMachine>();
		reservedSize = 0;
		reservedRAM = 0;
		reservedCPU = 0;
		running = false;
	}

	public double getConsumption() {
		if (running)
			return 200.0 + 0.3 * (getCurrentCPU() - initialCPU);
		else
			return 0.0;
	}

	// Creates a VirtualMachine of maximum possible Size
	public VirtualMachine startVirtualMachine(VMType type) {
		VirtualMachine vm = startVirtualMachine(maxSize - initialSize - VirtualMachine.initialSize,
				maxRAM - initialRAM - VirtualMachine.initialRAM, maxCPU - initialCPU
						- VirtualMachine.initialCPU, type);
		return vm;
	}

	public VirtualMachine startVirtualMachine(int sz, int ram, int cpu, VMType type) {
		if (checkVM(sz, ram, cpu)) {
			VirtualMachine vm = new VirtualMachine(sz, ram, cpu, this, type);
			VMs.put(vm.getId(), vm);
			reservedSize = reservedSize + vm.getReservedSize();
			reservedRAM = reservedRAM + vm.getReservedRAM();
			reservedCPU = reservedCPU + vm.getReservedCPU();
			totalVMs++;
			return vm;
		} else
			return null;
	}

	public void stopVirtualMachine(VirtualMachine vm) throws ActiveApplicationsException {
		if (VMs.containsKey(vm.getId())) {
			if (vm.getApplications().size() != 0) {
				throw new ActiveApplicationsException(
						"Applications must be migrated before stopping a VM, VM id " + vm.getId());
			} else {
				VMs.remove(vm.getId());
				reservedSize = reservedSize - vm.getReservedSize();
				reservedRAM = reservedRAM - vm.getReservedRAM();
				reservedCPU = reservedCPU - vm.getReservedCPU();
				totalResizeCalls += vm.getTotalResizeCalls();
			}
		}
	}

	/**
	 * Checks if there is enough space for the VM initial resources plus its netto resources
	 * @param size the _netto_ size reserved on the VM
	 * @param RAM the _netto_ RAM reserved on the VM
	 * @param CPU the _netto_ CPU reserved on the VM
	 * @return true if there is enough space to run the VM
	 */
	public boolean checkVM(int size, int RAM, int CPU) {
		return ((VirtualMachine.initialSize + size) <= availableSize())
				&& ((VirtualMachine.initialRAM + RAM) <= availableRAM())
				&& ((VirtualMachine.initialCPU + CPU) <= availableCPU());
	}

	public boolean resizeUp(int diffSize, int diffRAM, int diffCPU) throws VMResizeException {
		reservedSize = reservedSize + diffSize;
		reservedRAM = reservedRAM + diffRAM;
		reservedCPU = reservedCPU + diffCPU;
		return true;
	}

	public boolean resizeDown(Application app) {
		reservedSize = reservedSize - app.getSize();
		reservedRAM = reservedRAM - app.getRam();
		reservedCPU = reservedCPU - app.getCpu();
		return true;
	}

	public boolean checkExtendVM(int sizeDiff, int ramDiff, int CPUDiff) {
		return ((sizeDiff <= availableSize()) && (ramDiff <= availableRAM()) && (CPUDiff <= availableCPU()));
	}

	private int availableSize() {
		return maxSize - reservedSize;
	}

	private int availableRAM() {
		return maxRAM - reservedRAM;
	}

	private int availableCPU() {
		return maxCPU - reservedCPU;
	}

	public int getCurrentSize() {
		int currSize = initialSize;
		for (VirtualMachine vm : VMs.values())
			currSize += vm.getSize();
		return currSize;
	}

	public int getCurrentRam() {
		int currRAM = initialRAM;
		for (VirtualMachine vm : VMs.values())
			currRAM += vm.getRAM();
		return currRAM;
	}

	public int getCurrentCPU() {
		int currCPU = initialCPU;
		for (VirtualMachine vm : VMs.values())
			currCPU += vm.getCPU();
		return currCPU;
	}

	public double getSizeUtilization() {
		return ((double) (getCurrentSize() - initialSize) / (maxSize - initialSize)) * 100;
	}

	public double getRamUtilization() {
		return ((double) (getCurrentRam() - initialRAM) / (maxRAM - initialRAM)) * 100;
	}

	public double getCpuUtilization() {
		return ((double) (getCurrentCPU() - initialCPU) / (maxCPU - initialCPU)) * 100;
	}

	public double getAverageUtilization() {
		return (getSizeUtilization() + getRamUtilization() + getCpuUtilization()) / 3.0;
	}

	public int getId() {
		return id;
	}

	public boolean isRunning() {
		return running;
	}

	public VirtualMachine getVirtualMachine(int id) {
		return VMs.get(id);
	}

	public VirtualMachine getLatestVM() {
		VirtualMachine vm = null;
		for (Iterator<VirtualMachine> it = VMs.values().iterator(); it.hasNext();) {
			vm = it.next();
		}
		return vm;
	}

	public Integer getLatestVMID() {
		return getLatestVM().getId();
	}

	/**
	 * return a list of all VMs running on this PM.
	 *
	 * @return a HashMap with all VMs running on this PM.
	 */
	public HashMap<Integer, VirtualMachine> getVirtualMachines() {
		return VMs;
	}

	public int countCurrentlyRunningVMs() {
		return VMs.values().size();
	}

	public int getReservedSize() {
		return reservedSize;
	}

	public int getReservedRAM() {
		return reservedRAM;
	}

	public int getReservedCPU() {
		return reservedCPU;
	}

	public int getTotalVMs() {
		return totalVMs;
	}

	public int getTotalResizeCalls() {
		return totalResizeCalls;
	}

	@Override
	public int compareTo(PhysicalMachine other) {
		if (this == other)
			return EQUAL;

		if (getAverageUtilization() < other.getAverageUtilization())
			return BEFORE;
		else if (getAverageUtilization() > other.getAverageUtilization())
			return AFTER;
		else
			return EQUAL;
	}
}
