/**
 * This file is part of CloudML [ http://cloudml.org ]
 *
 * Copyright (C) 2012 - SINTEF ICT
 * Contact: Franck Chauvel <[email protected]>
 *
 * Module: root
 *
 * CloudML is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation, either version 3 of
 * the License, or (at your option) any later version.
 *
 * CloudML 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 Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General
 * Public License along with CloudML. If not, see
 * <http://www.gnu.org/licenses/>.
 */
package org.cloudml.deployer;

import com.amazonaws.util.StringInputStream;
import org.apache.commons.jxpath.JXPathContext;
import org.apache.commons.jxpath.JXPathNotFoundException;
import org.cloudml.codecs.JsonCodec;
import org.cloudml.connectors.Connector;
import org.cloudml.connectors.ConnectorFactory;
import org.cloudml.core.*;
import org.cloudml.core.actions.StandardLibrary;
import org.cloudml.core.collections.InternalComponentInstanceGroup;
import org.cloudml.core.collections.ProvidedExecutionPlatformGroup;
import org.cloudml.core.collections.RelationshipInstanceGroup;
import org.cloudml.mrt.Coordinator;

import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.*;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * Created by ferrynico on 02/12/2014.
 */
public class Scaler {

    private static final Logger journal = Logger.getLogger(Scaler.class.getName());

    protected Deployment currentModel;
    protected Coordinator coordinator;
    StandardLibrary lib = new StandardLibrary();
    VMInstance ci;
    protected CloudAppDeployer dep;

    public Scaler(Deployment currentModel, Coordinator coordinator, CloudAppDeployer dep){
        this.currentModel=currentModel;
        this.coordinator=coordinator;
        this.dep=dep;
    }


    protected VM findVMGenerated(String fromName, String extension){
        for(VM v: currentModel.getComponents().onlyVMs()){
            if(v.getName().contains(fromName) && v.getName().contains(extension)){
                return v;
            }
        }
        return null;
    }

    private VM createNewInstanceOfVMFromImage(VMInstance vmi){
        VM existingVM=vmi.asExternal().asVM().getType();
        //VM v=currentModel.getComponents().onlyVMs().firstNamed(existingVM.getName()+"-fromImage");
        VM v = findVMGenerated(existingVM.getName(),"fromImage");
        if(v == null){//in case a type for the snapshot has already been created
            String name=lib.createUniqueComponentInstanceName(currentModel,existingVM);
            v=new VM(name+"-fromImage",existingVM.getProvider());
            v.setGroupName(existingVM.getGroupName());
            v.setRegion(existingVM.getRegion());
            v.setImageId("tempID");
            v.setLocation(existingVM.getLocation());
            v.setMinRam(existingVM.getMinRam());
            v.setMinCores(existingVM.getMinCores());
            v.setMinStorage(existingVM.getMinStorage());
            v.setSecurityGroup(existingVM.getSecurityGroup());
            v.setSshKey(existingVM.getSshKey());
            v.setProviderSpecificTypeName(existingVM.getProviderSpecificTypeName());
            v.setPrivateKey(existingVM.getPrivateKey());
            v.setProvider(existingVM.getProvider());
            ProvidedExecutionPlatformGroup pepg=new ProvidedExecutionPlatformGroup();
            for(ProvidedExecutionPlatform pep: existingVM.getProvidedExecutionPlatforms()){
                ArrayList<Property> pg=new ArrayList<Property>();
                for(Property property: pep.getOffers()){
                    Property prop=new Property(property.getName());
                    prop.setValue(property.getValue());
                    pg.add(prop);
                }
                ProvidedExecutionPlatform p =new ProvidedExecutionPlatform(name+"-"+pep.getName(),pg);
                pepg.add(p);
            }
            v.setProvidedExecutionPlatforms(pepg.toList());
            currentModel.getComponents().add(v);
        }
        ci=lib.provision(currentModel,v).asExternal().asVM();
        return v;
    }

    private Map<InternalComponentInstance, InternalComponentInstance> duplicateHostedGraph(Deployment d, VMInstance vmiSource,VMInstance vmiDestination){
        //InternalComponentInstanceGroup icig= currentModel.getComponentInstances().onlyInternals().hostedOn(vmiSource);
        StandardLibrary lib=new StandardLibrary();
        return lib.replicateSubGraph(d, vmiSource, vmiDestination);
    }

    protected void manageDuplicatedRelationships(RelationshipInstanceGroup rig, Set<ComponentInstance> listOfAllComponentImpacted){
        if(rig != null){
            dep.configureWithRelationships(rig);
            for(RelationshipInstance ri: rig){
                listOfAllComponentImpacted.add(ri.getClientComponent());
                listOfAllComponentImpacted.add(ri.getServerComponent());
            }
        }
    }


    protected void configureBindingOfImpactedComponents(Set<ComponentInstance> listOfAllComponentImpacted, Map<InternalComponentInstance, InternalComponentInstance> duplicatedGraph){
        for(InternalComponentInstance ici: duplicatedGraph.values()){
            for(ProvidedPortInstance ppi: ici.getProvidedPorts()){
                RelationshipInstanceGroup rig=currentModel.getRelationshipInstances().whereEitherEndIs(ppi);
                manageDuplicatedRelationships(rig, listOfAllComponentImpacted);
            }
            for(RequiredPortInstance rpi: ici.getRequiredPorts()){
                RelationshipInstanceGroup rig=currentModel.getRelationshipInstances().whereEitherEndIs(rpi);
                manageDuplicatedRelationships(rig, listOfAllComponentImpacted);
            }
        }
    }

    protected void configureImpactedComponents(Set<ComponentInstance> listOfAllComponentImpacted, Map<InternalComponentInstance, InternalComponentInstance> duplicatedGraph){
        for(ComponentInstance ici: listOfAllComponentImpacted){
            coordinator.updateStatusInternalComponent(ici.getName(), InternalComponentInstance.State.INSTALLED.toString(), CloudAppDeployer.class.getName());
            if(ici.isInternal()){
                Provider p=ici.asInternal().externalHost().asVM().getType().getProvider();
                Connector c2=ConnectorFactory.createIaaSConnector(p);
                for(Resource r: ici.getType().getResources()){
                    dep.configure(c2, ci.getType(), ici.asInternal().externalHost().asVM(), r.getConfigureCommand(),false);
                }
                c2.closeConnection();
            }
            coordinator.updateStatusInternalComponent(ici.getName(), InternalComponentInstance.State.CONFIGURED.toString(), CloudAppDeployer.class.getName());
        }
    }

    protected void startImpactedComponents(Set<ComponentInstance> listOfAllComponentImpacted, Map<InternalComponentInstance, InternalComponentInstance> duplicatedGraph){
        for(ComponentInstance ici: listOfAllComponentImpacted){
            if(ici.isInternal()){
                Provider p=ici.asInternal().externalHost().asVM().getType().getProvider();
                Connector c2=ConnectorFactory.createIaaSConnector(p);
                for(Resource r: ici.getType().getResources()){
                    dep.start(c2,ci.getType(),ici.asInternal().externalHost().asVM(),r.getStartCommand());
                }
                c2.closeConnection();
            }
            coordinator.updateStatusInternalComponent(ici.getName(), InternalComponentInstance.State.RUNNING.toString(), CloudAppDeployer.class.getName());
        }

        for(InternalComponentInstance ici: duplicatedGraph.values()){
            coordinator.updateStatusInternalComponent(ici.getName(), InternalComponentInstance.State.RUNNING.toString(), CloudAppDeployer.class.getName());
        }
    }


    public void scaleOut(VMInstance vmi, int n){
        ArrayList<VM> newbies=new ArrayList<VM>();
        ArrayList<Map<InternalComponentInstance, InternalComponentInstance>> duplicatedGraphs=new ArrayList<Map<InternalComponentInstance, InternalComponentInstance>>();
        ArrayList<Thread> ts=new ArrayList<Thread>();
        ArrayList<VMInstance> cis=new ArrayList<VMInstance>();


        for(int i=0;i<n;i++) {
            VM temp = findVMGenerated(vmi.getType().getName(),"fromImage");
            VM v=createNewInstanceOfVMFromImage(vmi);
            newbies.add(v);
            Map<InternalComponentInstance, InternalComponentInstance> duplicatedGraph = duplicateHostedGraph(currentModel, vmi, ci);
            duplicatedGraphs.add(duplicatedGraph);
            cis.add(ci);
            if (temp == null) {
                Connector c = ConnectorFactory.createIaaSConnector(vmi.getType().getProvider());
                String ID = "";
                if(!vmi.getType().getProvider().getName().toLowerCase().equals("flexiant"))
                    ID=c.createImage(vmi);
                else {
                    ID=c.createImage(vmi);
                    restartHostedComponents(vmi);
                }
                c.closeConnection();
                v.setImageId(ID);
            } else {
                v.setImageId(temp.getImageId());
            }
        }

        for(int i=0;i<n;i++) {
            final Map<InternalComponentInstance, InternalComponentInstance> d=duplicatedGraphs.get(i);
            final VM vm=newbies.get(i);
            final String name=vmi.getName();
            final VMInstance ci=cis.get(i);
            ts.add(new Thread(){
                public void run() {
                    //once this is done we can work in parallel
                    Connector c2 = ConnectorFactory.createIaaSConnector(vm.getProvider());
                    HashMap<String, Object> result = c2.createInstance(ci);
                    c2.closeConnection();
                    coordinator.updateStatusInternalComponent(ci.getName(), result.get("status").toString(), CloudAppDeployer.class.getName());
                    coordinator.updateStatus(name, ComponentInstance.State.RUNNING, CloudAppDeployer.class.getName());
                    coordinator.updateIP(ci.getName(),result.get("publicAddress").toString(),CloudAppDeployer.class.getName());

                    setAllEnvVarComponent(ci,currentModel);

                    //4. configure the new VM
                    //execute the configuration bindings
                    Set<ComponentInstance> listOfAllComponentImpacted= new HashSet<ComponentInstance>();
                    configureBindingOfImpactedComponents(listOfAllComponentImpacted,d);

                    //execute configure commands on the components
                    configureImpactedComponents(listOfAllComponentImpacted,d);

                    //execute start commands on the components
                    startImpactedComponents(listOfAllComponentImpacted, d);

                    //restart components on the VM scaled
                    restartHostedComponents(ci);
                }
            });
            ts.get(i).start();
        }

        for(Thread t: ts){
            try {
                t.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        journal.log(Level.INFO, ">> Multiple scaling completed!");
    }

    private void setAllEnvVarComponent(VMInstance ci, Deployment d){
        Map<String, String> env = System.getenv();
        String ip="";
        String port="";
        if(env.containsKey("MODACLOUDS_MONITORING_MANAGER_ENDPOINT_IP")
                && env.containsKey("MODACLOUDS_MONITORING_MANAGER_ENDPOINT_PORT")) {
            ip = env.get("MODACLOUDS_MONITORING_MANAGER_ENDPOINT_IP");
            port = env.get("MODACLOUDS_MONITORING_MANAGER_ENDPOINT_PORT");
        }else if(env.containsKey("MODACLOUDS_TOWER4CLOUDS_MANAGER_ENDPOINT_IP") &&
                env.containsKey("MODACLOUDS_TOWER4CLOUDS_MANAGER_ENDPOINT_PORT")){
            ip = env.get("MODACLOUDS_TOWER4CLOUDS_MANAGER_ENDPOINT_IP");
            port = env.get("MODACLOUDS_TOWER4CLOUDS_MANAGER_ENDPOINT_PORT");
        }else if(env.containsKey("MODACLOUDS_TOWER4CLOUDS_MANAGER_PUBLIC_ENDPOINT_IP") &&
                env.containsKey("MODACLOUDS_TOWER4CLOUDS_MANAGER_PUBLIC_ENDPOINT_PORT")){
            ip = env.get("MODACLOUDS_TOWER4CLOUDS_MANAGER_PUBLIC_ENDPOINT_IP");
            port = env.get("MODACLOUDS_TOWER4CLOUDS_MANAGER_PUBLIC_ENDPOINT_PORT");
        }else{
            try {
                ip= InetAddress.getLocalHost().getHostAddress();
                port="8170";
            } catch (UnknownHostException e) {
                e.printStackTrace();
            }
        }
        String cmd="";
        cmd+=setEnvVarCommand(ci, "MODACLOUDS_TOWER4CLOUDS_MANAGER_IP", ip);
        cmd+=setEnvVarCommand(ci, "MODACLOUDS_TOWER4CLOUDS_MANAGER_PORT", port);
        for(InternalComponentInstance ici: ci.hostedComponents()){
            for(Property p : ici.getProperties()){
                if(p.getName().startsWith("env:")){
                    cmd+=prepareSetEnv(d, ici, p);
                }
            }
            InternalComponent ic = ici.getType();
            for(Property p : ic.getProperties()){
                if(p.getName().contains("env:")){
                    cmd+=prepareSetEnv(d, ici, p);
                }
            }
        }
        setEnvVar(ci,cmd);

    }

    //TODO: All this code is replicated and should be refactored in the deployer
    private String setEnvVarCommand(VMInstance vmi, String varName, String value){
        if (!vmi.getType().getOs().toLowerCase().contains("windows")) {
            //String command="echo export "+varName+"="+value+" >> ~/.bashrc";
            //jc.execCommand(vmi.getId(), command, "ubuntu", vmi.getType().getPrivateKey());
            return "sudo sh -c 'echo export "+varName+"="+value+" >> /etc/environment';";

        } else {
            //TODO: should we do something for Windows as well?
        }
        return "";
    }

    private void setEnvVar(VMInstance vmi, String cmd){
        if (!vmi.getType().getOs().toLowerCase().contains("windows")) {
            //String command="echo export "+varName+"="+value+" >> ~/.bashrc";
            Connector jc = ConnectorFactory.createIaaSConnector(vmi.getType().getProvider());
            jc.execCommand(vmi.getId(), cmd, "ubuntu", vmi.getType().getPrivateKey());
            jc.closeConnection();
        } else {
            //TODO: should we do something for Windows as well?
        }
    }


    private String prepareSetEnv(Deployment d, InternalComponentInstance c, Property p){
        String value="";
        if(p.getValue().startsWith("$")){
            if(p.getValue().equals("${this.host.id}")){
                value=c.externalHost().asVM().getId();
            }
            if(p.getValue().equals("${this.host.name}")){
                value=c.externalHost().getName();
            }
            if(p.getValue().equals("${this.host.type.name}")){
                value=c.externalHost().getType().getName();
            }
            if(p.getValue().equals("${this.provider.id}")){
                value=c.externalHost().asVM().getType().getProvider().getName();
            }
            if(p.getValue().equals("${this.name}") || p.getValue().equals("${this.id}")){
                value=c.getName();
            }
            if(p.getValue().equals("${this.type.name}")){
                value=c.getType().getName();
            }
        }else{
            try{
                JXPathContext jxpc = JXPathContext.newContext(d);
                Object o=jxpc.getValue(p.getValue());
                value=o.toString();
            }catch(NullPointerException e){
                journal.log(Level.INFO, ">> Environment variable cannot be defined, xpath expression not valid");
            }
            catch(JXPathNotFoundException e){
                journal.log(Level.INFO, ">> Environment variable cannot be defined, xpath expression not valid");
            }
        }
        if(!value.equals("")){
            return setEnvVarCommand(c.externalHost().asVM(), p.getName().split(":")[1], value);
        }
        return "";
    }

    /**
     * Method to scale out a VM within the same provider
     * Create a snapshot of the VM and then configure the bindings
     *
     * @param vmi an instance of VM
     */
    public void scaleOut(VMInstance vmi) {
        Deployment tmp=currentModel.clone();
        VM temp = findVMGenerated(vmi.getType().getName(),"fromImage");
        //1. instantiate the new VM using the newly created snapshot
        VM v=createNewInstanceOfVMFromImage(vmi);

        //2. update the deployment model by cloning the PaaS and SaaS hosted on the replicated VM
        Map<InternalComponentInstance, InternalComponentInstance> duplicatedGraph=duplicateHostedGraph(currentModel,vmi, ci);

        //3. For synchronization purpose we provision once the model has been fully updated
        if(temp == null){
            Connector c = ConnectorFactory.createIaaSConnector(vmi.getType().getProvider());
            String ID="";
            if(!vmi.getType().getProvider().getName().toLowerCase().equals("flexiant"))
                ID=c.createImage(vmi);
            else{
                ID=c.createImage(vmi);
                restartHostedComponents(vmi);
            }
            c.closeConnection();
            v.setImageId(ID);
        }else{
            v.setImageId(temp.getImageId());
        }

        Connector c2=ConnectorFactory.createIaaSConnector(v.getProvider());
        HashMap<String,Object> result=c2.createInstance(ci);

        c2.closeConnection();
        coordinator.updateStatusInternalComponent(ci.getName(), result.get("status").toString(), CloudAppDeployer.class.getName());
        coordinator.updateStatus(vmi.getName(), ComponentInstance.State.RUNNING, CloudAppDeployer.class.getName());
        coordinator.updateIP(ci.getName(),result.get("publicAddress").toString(),CloudAppDeployer.class.getName());

        dep.setAllEnvVarComponent(currentModel);

        //4. configure the new VM
        //execute the configuration bindings
        Set<ComponentInstance> listOfAllComponentImpacted= new HashSet<ComponentInstance>();
        configureBindingOfImpactedComponents(listOfAllComponentImpacted,duplicatedGraph);

        //execute configure commands on the components
        configureImpactedComponents(listOfAllComponentImpacted,duplicatedGraph);

        //execute start commands on the components
        startImpactedComponents(listOfAllComponentImpacted, duplicatedGraph);

        //restart components on the VM scaled 
        restartHostedComponents(ci);

        journal.log(Level.INFO, ">> Scaling completed!");
    }

    private ArrayList<InternalComponentInstance> allHostedComponents(ComponentInstance ci){
        ArrayList<InternalComponentInstance> list=new ArrayList<InternalComponentInstance>();
        InternalComponentInstanceGroup icig=ci.hostedComponents();
        if(icig !=null){
            list.addAll(icig);
            for(InternalComponentInstance ici: icig){
                list.addAll(allHostedComponents(ici));
            }
        }
        return list;
    }

    protected void restartHostedComponents(VMInstance ci){
        journal.log(Level.INFO, ">> Restarting Hosted components of: "+ci.getName());
        for(InternalComponentInstance ici: allHostedComponents(ci)){
            Provider p=ci.getType().getProvider();
            Connector c2=ConnectorFactory.createIaaSConnector(p);
            for(Resource r: ici.getType().getResources()){
                dep.start(c2,ci.getType(),ci,r.getStartCommand());
            }
            c2.closeConnection();
        }
    }


    private List<VM> vmFromAProvider(Provider p){
        ArrayList<VM> result=new ArrayList<VM>();
        for(VM v : currentModel.getComponents().onlyVMs()){
            if(v.getProvider().getName().equals(p.getName())){
                result.add(v);
            }
        }
        return result;
    }

    private VM findSimilarVMFromProvider(VM sampleVM, Provider p){
        VM selected=null;
        List<VM> availablesVM=vmFromAProvider(p);
        if(availablesVM.size() > 0){
            selected=availablesVM.get(0);
            for(VM v: availablesVM){
                if((v.getMinRam() >= sampleVM.getMinRam()) && (v.getMinRam() < selected.getMinRam())){
                    selected=v;
                }
            }
        }
        return selected;
    }


    public Deployment scaleOut(ExternalComponentInstance eci, Provider provider){
        if(eci.isVM()){
            scaleOut(eci.asVM(),provider);
            return currentModel;
        }else{
            Deployment targetModel=cloneCurrentModel();
            ExternalComponentInstance eci2=targetModel.getComponentInstances().onlyExternals().firstNamed(eci.getName());
            ExternalComponent ec=eci2.getType().asExternal();
            ec.setProvider(provider);
            eci2.setStatus(ComponentInstance.State.STOPPED);
            dep.deploy(targetModel);
            return targetModel;
        }
    }


    private Deployment cloneCurrentModel(){
        //need to clone the model
        JsonCodec jsonCodec=new JsonCodec();
        ByteArrayOutputStream baos=new ByteArrayOutputStream();
        jsonCodec.save(currentModel,baos);

        Deployment targetModel=new Deployment();
        try {
            String aString = new String(baos.toByteArray(),"UTF-8");
            InputStream is = new StringInputStream(aString);
            targetModel = (Deployment) jsonCodec.load(is);
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
        return targetModel;
    }

    /**
     * To scale our a VM on another provider (kind of bursting)
     *
     * @param vmi      the vm instance to scale out
     * @param provider the provider where we want to burst
     */
    public void scaleOut(VMInstance vmi, Provider provider) {
        Connector c = ConnectorFactory.createIaaSConnector(provider);
        StandardLibrary lib = new StandardLibrary();

        //need to clone the model
        Deployment targetModel=cloneCurrentModel();

        VM existingVM=findSimilarVMFromProvider(vmi.asExternal().asVM().getType(), provider);
        if(existingVM == null){
            journal.log(Level.INFO, ">> No VM available for this provider!");
            return;
        }

        //VM existingVM=vmi.asExternal().asVM().getType();
        //VM v=currentModel.getComponents().onlyVMs().firstNamed(existingVM.getName()+"-scaled");
        VM v = findVMGenerated(existingVM.getName(), "scaled");
        if(v == null){//in case a type for the snapshot has already been created
            String name=lib.createUniqueComponentInstanceName(targetModel,existingVM);
            v=new VM(name+"-scaled",provider);
            v.setGroupName(existingVM.getGroupName());
            v.setRegion(existingVM.getRegion());
            v.setImageId(existingVM.getImageId());
            v.setLocation(existingVM.getLocation());
            v.setMinRam(existingVM.getMinRam());
            v.setMinCores(existingVM.getMinCores());
            v.setMinStorage(existingVM.getMinStorage());
            v.setSecurityGroup(existingVM.getSecurityGroup());
            v.setSshKey(existingVM.getSshKey());
            v.setPrivateKey(existingVM.getPrivateKey());
            v.setProvider(provider);
            ProvidedExecutionPlatformGroup pepg=new ProvidedExecutionPlatformGroup();
            for(ProvidedExecutionPlatform pep: existingVM.getProvidedExecutionPlatforms()){
                ArrayList<Property> pg=new ArrayList<Property>();
                for(Property property: pep.getOffers()){
                    Property prop=new Property(property.getName());
                    prop.setValue(property.getValue());
                    pg.add(prop);
                }
                ProvidedExecutionPlatform p =new ProvidedExecutionPlatform(name+"-"+pep.getName(),pg);
                pepg.add(p);
            }
            v.setProvidedExecutionPlatforms(pepg.toList());
            targetModel.getComponents().add(v);
        }

        ci=lib.provision(targetModel,v).asExternal().asVM();

        //2. update the deployment model by cloning the PaaS and SaaS hosted on the replicated VM
        Map<InternalComponentInstance, InternalComponentInstance> duplicatedGraph=duplicateHostedGraph(targetModel,vmi, ci);

        dep.deploy(targetModel);
    }

}