package org.ut.cmldep;

import com.amazonaws.services.ec2.model.InstanceType;
import com.google.inject.Singleton;
import org.cloudml.core.*;
import org.cloudml.core.collections.VMInstanceGroup;
import org.cloudml.deployer.CloudAppDeployer;
import org.ut.cmldep.amazon.request.AmazonRequest;
import org.ut.cmldep.json.parser.InstanceToCreate;
import org.ut.cmldep.json.parser.NewConfiguration;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Random;


/**
 * Created by Iurii on 12/11/2014.
 */

@Singleton
public class CMLDeployer {

	private static CMLDeployer istance = null;

    public static final String PREF_EXEC = "exec-";
    public static final String PREF_REL = "rel-";
    
    public static final String LOCATION = "us-east-1"; // Virginia
    public static final String SECURITY_GROUP = "iurii-open-all-tcp";
    public static final String GROUP_NAME = "iurii-cloudml-scalable";
    public static final String IMAGE_ID = "us-east-1/ami-988ad1f0";
    public static final String PRIVATE_KEY = "C:\\Project Cloud Lab\\iuriieast1.pem";
    public static final String SHH_KEY = "iurii.east1";

	private static String status_URI = "/nginx_status";

    private CloudAppDeployer deployer = null;

	private Deployment currentDeployment = null;
    
    private List<String> listOfTags = new ArrayList<String>();
	private InstanceToCreate instanceNodeType;

	private int count = 0;

	public CMLDeployer(){

		setListOfTags(AmazonRequest.getAllTags());
		setDeployer(new CloudAppDeployer());
    }

	public synchronized static CMLDeployer getSingleton() {
		if (istance == null)
			istance = new CMLDeployer();
		return istance;
	}
	
	public boolean start(){
		
		boolean res = true;
		try{
			deployer.deploy(new InitialDeploymentConfiguration().getDeployment());

			System.out.println("Initial deployment finished !!!");
			//deployer.setCurrentModel(new InitialDeploymentConfiguration().getDeployment(getCredentials()));
		}catch(Exception ex){
			ex.printStackTrace();
			res = false;
		}
		
		return res;
		
	}

	public boolean startSecondExperement(){

		boolean res = true;
		try{
			deployer.deploy(new InitialDeploymentConfiguration().getDeployment());

			System.out.println("Initial deployment finished !!!");
			//deployer.setCurrentModel(new InitialDeploymentConfiguration().getDeployment(getCredentials()));
		}catch(Exception ex){
			ex.printStackTrace();
			res = false;
		}

		return res;

	}

	/**
	 * Scale up elements of current deployment. Scaling possible only for components that are exists in Initial Deployment Model.
	 * 
	 * @param config Description of the requested configuration.
	 */
    public void scaleUp(NewConfiguration config){
    	    	
        Deployment newDeployment = getModel().clone();
		deployer.deploy(newDeployment);
    	System.out.println("Start scale up");
    }

	public void scaleUp(String instance){
		deployer.scaleUp(instance);
		System.out.println("Start scale up");
	}

	public void scaleDown(String instance){
		deployer.scaleDown(instance);
		System.out.println("Start scale up");
	}

	public void scaleUpTest(int[] configuration){
		deployer.scaleUpTest(configuration);
		System.out.println("Start scale up test");
	}

	public void scaleDownTest(int[] configuration){
		deployer.scaleDownTest(configuration);
		System.out.println("Start scale up");
	}

	public void startInitialSetUp(){

		this.start();
	}

	public void startExperiment(){

		this.start();
		deployer.initiateAllLoadBalancers();
	}

	public void scale(int[] configuration){
		deployer.scaleGeneralTest(configuration);
	}


	public int getServerLoad(ScalableComponent scalableComponent){
		int totalReqs = 0;

		String ip = "";

		for (ScalableComponent sc : deployer.getCurrentModel().getScalableComponents()){
			if(sc.equals(scalableComponent)){
				ip = sc.getLoadBalancer().getVmInstance().getPublicAddress();
			}
		}

		try {

			URL page = new URL("http://" + ip + status_URI);

			BufferedReader reader = new BufferedReader(
					new InputStreamReader(page.openStream()));

			String line;
			while( (line = reader.readLine()) != null){

				if(line.startsWith(" ")){
					String[] col = line.trim().split(" ");
					totalReqs = Integer.parseInt(col[2]);
				}
			}

			reader.close();
		} catch (IOException e) {
			System.out.println("Cannot fetch statistics from nginx status page IP: " + ip);
		}

		return totalReqs > 0 ? (totalReqs - 1) : totalReqs;
	}

    /**
     * Scale down deployment model. Achieved by killing instances.
     * 
     * @param instancesToDelete List of Amazon instances id that need to be killed.
     */
    public void scaleDown(List<String> instancesToDelete){
    	
    	Deployment newDeployment = getModel().clone();
    	
    	
    	for (String instanceID : instancesToDelete){
    		    		
    		VMInstance instance = newDeployment.getComponentInstances().onlyVMs().withID(LOCATION + "/" + instanceID);
    		
    		newDeployment.getComponentInstances().remove(instance);
    		
    		newDeployment.getExecuteInstances().remove(
    				newDeployment.getExecuteInstances().firstNamed(PREF_EXEC + instance.getName()));
    		
    	}
    	
        deployer.deploy(newDeployment);
    }

    private InternalComponentInstance findInternalComponentInstance(String name){
        return getModel().getComponentInstances().onlyInternals().firstNamed(name);
    }

    private InternalComponent findComponentByName(String name){
        return getModel().getComponents().onlyInternals().firstNamed(
        		getComponentNameFromComponentInstanceName(name));
    }

    private VM findVM(String sizeName){
    	
    	if (getModel().getComponents().onlyVMs().contains(sizeName)){
    		return getModel().getComponents().onlyVMs().firstNamed(sizeName);
    	}else{
    		return createVM(sizeName);
    	}
    }
    
    private Relationship findRelationship(){
    	return null;
    } 
    
    private InternalComponent findInternalComponent(String name){
    	return null;
    }
    
    private InternalComponentInstance createInternalComponentInstance(){
    	return null;
    }
    
    private RelationshipInstance createRelationshipInstance(){
    	return null;
    }
    
    /**
     * Create new InternalComponentInstance with name that doesn't occupied yet
     * 
     * @param component InternalComponent that is type for new InternalComponentInstance;
     * @return new InternalComponentInstance with unique name
     */
    private InternalComponentInstance createComponentInstance(InternalComponent component){
    	
    	int i = 1;
    	
    	List<String> names = new ArrayList<String>();
    	
    	for (InternalComponentInstance ici : getModel().getComponentInstances().onlyInternals()){
    		names.add(ici.getName());
    	}
    	
    	while(names.contains(component.getName() + "--" + String.valueOf(i))){
    		i++;
    	}
    	
    	return new InternalComponentInstance(component.getName() + "--" + String.valueOf(i), component);	
    }
    
    /**
     * 
     * 
     * @param toSclae
     * @param newOne
     * @return
     */
	private List<RelationshipInstance> cloneRelationshipRequired(InternalComponentInstance toSclae, InternalComponentInstance newOne){
		
		List<RelationshipInstance> list = new ArrayList<RelationshipInstance>();
		
		for (RelationshipInstance rxins : getModel().getRelationshipInstances().toList()){
			if (rxins.isProvidedBy(toSclae)){
				list.add(new RelationshipInstance(
							"some_name",
							newOne.getRequiredPorts().ofType(rxins.getRequiredEnd().getType()),
							rxins.getProvidedEnd(),
							rxins.getType()
						)
				);
			}
		}
		
		return list;
	}
	
	/**
	 * 
	 * @param toSclae
	 * @param newOne
	 * @return
	 */
	private List<RelationshipInstance> cloneRelationshipProvided(InternalComponentInstance toSclae, InternalComponentInstance newOne){
		
		List<RelationshipInstance> list = new ArrayList<RelationshipInstance>();
		
		for (RelationshipInstance rxins : getModel().getRelationshipInstances().toList()){
			if (rxins.isProvidedBy(toSclae)){
				list.add(new RelationshipInstance(
							"some_name",
							rxins.getRequiredEnd(), 
							newOne.getProvidedPorts().ofType(rxins.getProvidedEnd().getType()),
							rxins.getType()
						)
				);
			}
		}
		
		return list;
	}
    
    
    
    private VM createVM(String size){
    	
    	VM newVM = new VM(size, getModel().getProviders().iterator().next());
    	
    	ProvidedExecutionPlatform vmOS = new ProvidedExecutionPlatform("vmOS");
        vmOS.setProperty("OS", "Ubuntu");

        newVM.setProviderSpecificTypeName(InstanceType.valueOf(size).toString());
        newVM.setLocation(LOCATION);
        newVM.setSshKey(SHH_KEY);
        newVM.setSecurityGroup(SECURITY_GROUP);
        newVM.setGroupName(GROUP_NAME);
        newVM.setPrivateKey(PRIVATE_KEY);
        newVM.setImageId(IMAGE_ID);
        newVM.setIs64os(true);
        newVM.getProvidedExecutionPlatforms().add(vmOS);
        
        return newVM;	    	
    }
    
    private void cloneComponentInstance(InternalComponent component, Deployment deplyment){
    	
    	InternalComponentInstance newInternalComponentInstance = createComponentInstance(component);
    	VMInstance newVMInstance = createVMInstance(newInternalComponentInstance.getName(), instanceNodeType.getSize());

    	deplyment.getComponentInstances().add(newVMInstance);
    	deplyment.getComponentInstances().add(newInternalComponentInstance);
        
        if (!deplyment.getComponents().onlyVMs().contains(newVMInstance.getType())){
        	deplyment.getComponents().add(newVMInstance.getType());
        }
    }
    
    private ExecuteInstance createExecuteInstance(InternalComponentInstance internalComponentInstance,
                                                  InternalComponent internalComponent,
                                                  VMInstance vmInstance){

        return new ExecuteInstance(findPossibleName(PREF_EXEC + internalComponentInstance.getName(),
                getModel().getExecuteInstances().onlyNames()),
                internalComponentInstance.getRequiredExecutionPlatform(),
                vmInstance.getProvidedExecutionPlatforms().firstMatchFor(internalComponent));
    }

    private VMInstance createVMInstance(String componentName, String vmSizeName){
        return new VMInstance(
        		findPossibleTagForInstance(componentName, getModel().getComponentInstances().onlyVMs().onlyNames()),
        		findVM(vmSizeName));
    }
    
    private int countExistionInstances(int newAmount, String sizeOfInstance, String typeOfInstance){
    	    	
    	int count = 0;
    	
    	VMInstanceGroup group = getModel().getComponentInstances().onlyVMs();
    	
    	for (VMInstance instance : group){
    		
    		if (instance.getName().indexOf(typeOfInstance) == 0 && instance.getType().getName().equals(sizeOfInstance)) 
    			count += 1;
    	}
    	return newAmount - count;
    
    }

    /**
     * Return name that is not in the list
     *
     * @param name Part of the name that you want to use
     * @param namesInUse List of names that are already in use
     * @return Name that contain of name + "-" + random integer that is not in the list
     */
    private String findPossibleName(String name, Collection<String> namesInUse){

        Random random = new Random();
        int randomNumber = random.nextInt(3000 - 1000) + 1000;

        while(namesInUse.contains(name + "-" + String.valueOf(randomNumber))){
            randomNumber = random.nextInt(3000 - 1000) + 1000;
        }

        return name + "-" + String.valueOf(randomNumber);
    }
    
    
    private String findPossibleTagForInstance(String name, Collection<String> namesInUse){
    	
    	Random random = new Random();
        int randomNumber = random.nextInt(3000 - 1000) + 1000;

        while(namesInUse.contains(name + "-" + String.valueOf(randomNumber)) ||
        		getListOfTags().contains(name + "-" + String.valueOf(randomNumber))){
            randomNumber = random.nextInt(3000 - 1000) + 1000;
        }

        return name + "-" + String.valueOf(randomNumber);
    }


    /**
     * Returns Component name from Component Instance name.
     * Component name should ALWAYS follow template component_name + "--" + number.
     *
     * @param name Name of ComponentInstance
     * @return String name of Component
     */
    private String getComponentNameFromComponentInstanceName(String name){
        return name.split("--")[0];
    }
   
    public void setDeployer(CloudAppDeployer deployer) {
		this.deployer = deployer;
	}
    
    public Deployment getModel(){
    	return deployer.getCurrentModel();
    }

	public List<String> getListOfTags() {
		return listOfTags;
	}

	public void setListOfTags(List<String> listOfTags) {
		this.listOfTags = listOfTags;
	}
	
	
}
