package org.ut.cmldep.experiments;

import org.cloudml.core.ScalableComponent;
import org.cloudml.core.VMInstance;
import org.ut.cmldep.CMLDeployer;
import org.ut.cmldep.Optimj.LPsolution;
import org.ut.cmldep.Optimj.LPsolution_GLPK_Opt;

import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.List;

/**
 * Created by Iurii on 4/27/2015.
 */
public class Experiment implements Runnable {

    private long lastUpdateTime = 0;
    private int[] lastWorkLoad = null;

    private static int HOUR = 60;
    private static int NUMBER_TIME_UNITS = 60;

    private static String[] TYPES_OF_INSTANCES = new String[]{"m1.small", "m3.medium", "m3.large"};
    private static double[] COST_PER_HOUR = new double[]{0.044, 0.067, 0.175};
    private static int[] PROCESSING_POWER = new int[]{15, 30, 65};
    private static int[] CAPACITY_OF_REGIONS = new int[]{20, 20, 20};
    private static int START_TIME = 3;
    private static int MAX_NUMBER_OF_INSTANCES = 20;


    private int timeBags[] = null;
    private int numberOfRegions = 0;
    private int delay = 1;

    CMLDeployer cmlDeployer = null;

    public Experiment(){
        this.lastUpdateTime = 0;
        this.lastWorkLoad = new int[calcNumberOfRegions()];
    }

    public void run() {

        int[] configuration = null;
        int[] workLoad = null;
        int[] indexForInstances = null;
        delay = CMLDeployer.getSingleton().getModel().getDelay() * 60;

        DateFormat dateFormat = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss");
        Date date = new Date();
        try {

            this.setNumberOfRegions(calcNumberOfRegions());
            this.setTimeBags(getArrayOfTimeBags());


            int numOfTimeBags = getTotalNumOfCurrentTimeBags();
            int[] startTime = getStartTimeOfEachInstanceType();
            int numberOfInstances = getNumberOfInstance();
            double[] killingCost = getKillingCost();
            double[] retainingCost = getRetainingCost();
            int[] maxOfEach = getMaxNumOfEachInstanceType();
            workLoad = getWorkLoad();
            int[] indexForRegion = getIndexForRegions();
            indexForInstances = getInstanceTypesInRegionsIndex();

            LPsolution lps = null;

            lps = new LPsolution_GLPK_Opt(
                    this.getNumberOfRegions(),
                    NUMBER_TIME_UNITS,
                    numOfTimeBags,
                    TYPES_OF_INSTANCES.length,
                    this.getTimeBags(),
                    COST_PER_HOUR,
                    startTime,
                    killingCost,
                    retainingCost,
                    PROCESSING_POWER,
                    maxOfEach,
                    workLoad,
                    CAPACITY_OF_REGIONS,
                    indexForRegion,
                    indexForInstances);

            lps.resolve();
            configuration = lps.getN();

            System.out.println("================ Solution ==================");
            System.out.println("Time: " + dateFormat.format(date));
            System.out.println("Solution: " + Arrays.toString(configuration));
            System.out.println("Work load: " + Arrays.toString(workLoad));
            System.out.println("Killing cost: " + Arrays.toString(killingCost));
            System.out.println("Retaining cost: " + Arrays.toString(retainingCost));
            System.out.println("Number of time bags: " + String.valueOf(numOfTimeBags));
            System.out.println("Index of instance: " + Arrays.toString(indexForInstances));
            System.out.println("******************* Time Bags ***********************");
            System.out.println("Solution: " + Arrays.toString(this.getTimeBags()));
            System.out.println("------------------------------------------------------");



            //CMLDeployer.getSingleton().scale(configuration);

        }catch (Exception ex){
            System.out.println("Error in Experiment !!!");
            System.out.println(ex.fillInStackTrace());
            System.out.println(ex.getMessage());
        }

        System.out.println("Number of instance in the model: " + String.valueOf(getCmlDeployer().getModel().getComponentInstances().onlyVMs().size()));

    }

    private int getNumberOfInstance() {
        return getCmlDeployer().getModel().getComponentInstances().onlyVMs().size() -
                getCmlDeployer().getModel().getScalableComponents().size() - getCmlDeployer().getModel().getScalableComponents().size();
    }

    private int[] getArrayOfTimeBags() {

        int[] timeBuckets = new int[this.getNumberOfRegions() * TYPES_OF_INSTANCES.length * NUMBER_TIME_UNITS];
        int scalableComponentIndex = 0;

        System.out.println("===================== Debag time bags ================ ");

        for (ScalableComponent scalableComponent : getCmlDeployer().getModel().getScalableComponents()) {

            for (int j = 0; j < TYPES_OF_INSTANCES.length; j++) {

                List<VMInstance> listOfVmInstance = new ArrayList<VMInstance>();
                for (VMInstance vmInstance : scalableComponent.getListOfVMInstancesInComponent()) {
                    if (vmInstance.getType().getProviderSpecificTypeName().equals(TYPES_OF_INSTANCES[j])) {
                        listOfVmInstance.add(vmInstance);
                    }
                }

                System.out.println("==== index =======");

                for (VMInstance vmInstance : listOfVmInstance) {
                    int index = Math.round(
                            (scalableComponentIndex * NUMBER_TIME_UNITS * TYPES_OF_INSTANCES.length) +
                                    (j * NUMBER_TIME_UNITS) +
                                    (((System.currentTimeMillis() - vmInstance.getStartTime()) / (60 * 1000)) % HOUR));
                    timeBuckets[index]++;

                    System.out.println("Instance Start time: " + String.valueOf(vmInstance.getStartTime() / (60 * 1000)));
                    System.out.println("Current time: " + String.valueOf(System.currentTimeMillis() / (60 * 1000)));
                    System.out.println("Scalable component index: " + String.valueOf(scalableComponentIndex));
                    System.out.println("Type of instance index: " + String.valueOf(j));
                    System.out.println("Index is: " + String.valueOf(index));
                }
            }
            scalableComponentIndex++;
        }
        return timeBuckets;
    }

    private int[] getWorkLoad() {

        long currentTime = System.currentTimeMillis();
        int[] rawWorkLoad = new int[this.getNumberOfRegions()];
        int[] workLoad = new int[this.getNumberOfRegions()];
        List<ScalableComponent> scalableComponents = getCmlDeployer().getModel().getScalableComponents().toList();

        for (int i = 0; i < this.getNumberOfRegions(); i++) {
            rawWorkLoad[i] = getCmlDeployer().getServerLoad(scalableComponents.get(i));
        }

        System.out.println("=============== Work load============");
        System.out.println("Current work load: " + Arrays.toString(rawWorkLoad));
        System.out.println("Last work load: " + Arrays.toString(lastWorkLoad));

        System.out.println("Current time: " + String.valueOf(currentTime / (1000)));
        System.out.println("Last updated time: " + String.valueOf(lastUpdateTime / 1000));
        System.out.println("Time diff: " + String.valueOf(currentTime - lastUpdateTime));


        if (lastUpdateTime != 0) {
            for (int i = 0; i < this.getNumberOfRegions(); i++) {
                workLoad[i] = Math.round((rawWorkLoad[i] - lastWorkLoad[i]) / ((currentTime - lastUpdateTime) / 1000));
            }
        } else {

            for(int i = 0; i < rawWorkLoad.length; i++ ){
                workLoad[i] = rawWorkLoad[i] / this.delay;
            }

        }

        this.lastUpdateTime = currentTime;
        this.lastWorkLoad = rawWorkLoad;

        System.out.println("Updated work load: " + Arrays.toString(workLoad));

        return workLoad;
    }

    private int[] getIndexForRegions() {

        int indexOfRegions[] = new int[this.getNumberOfRegions() + 1];

        indexOfRegions[0] = 0;

        int sum = 0;
        for (int i = 1; i < this.getNumberOfRegions() + 1; i++) {
            sum = indexOfRegions[i - 1] + TYPES_OF_INSTANCES.length; //TODO: only active number of instance types
            indexOfRegions[i] = sum;
        }

        return indexOfRegions;
    }

    private int[] getInstanceTypesInRegionsIndex() {

        int indexOfInstanceTypesInRegions[] = new int[getTotalNumOfInstanceTypesInRegions() + 1];
        int index = 0;
        indexOfInstanceTypesInRegions[index] = 0;
        int sum = 0;

        int[] timeBags = getTimeBags();

        for (int i = 0; i < this.getNumberOfRegions(); i++) {
            for (int j = 0; j < TYPES_OF_INSTANCES.length; j++) {

                int numberOfNotEmptyBagsInRegion = 0;

                for (int k = 0; k < NUMBER_TIME_UNITS; k++) {
                    if (timeBags[(i * NUMBER_TIME_UNITS * TYPES_OF_INSTANCES.length) + (j * NUMBER_TIME_UNITS) + k] != 0) {
                        numberOfNotEmptyBagsInRegion++;
                    }
                }
                index++;
                indexOfInstanceTypesInRegions[index] = numberOfNotEmptyBagsInRegion + indexOfInstanceTypesInRegions[index - 1];

            }
        }

        return indexOfInstanceTypesInRegions;
    }

    private int getTotalNumOfInstanceTypesInRegions() {

        int totalNumOfInstanceTypesInRegions = 0;
        for (int i = 0; i < this.getNumberOfRegions(); i++) {
            totalNumOfInstanceTypesInRegions += TYPES_OF_INSTANCES.length;
        }

        return totalNumOfInstanceTypesInRegions;
    }

    private int getTotalNumOfCurrentTimeBags() {

        int numberOfBags = 0;
        int[] timeBags = this.getTimeBags();


        for (int i = 0; i < timeBags.length; i++) {
            if (timeBags[i] != 0) {
                numberOfBags++;
            }
        }

        return numberOfBags;
    }

    private int calcNumberOfRegions() {
        return getCmlDeployer().getModel().getScalableComponents().size();
    }

    private double[] getKillingCost() {

        int index = 0;

        double[] bck = new double[getTotalNumOfCurrentTimeBags()];

        int[] timeBags = this.getTimeBags();

        for (int i = 0; i < this.getNumberOfRegions(); i++) {
            for (int j = 0; j < TYPES_OF_INSTANCES.length; j++) {
                for (int k = 0; k < NUMBER_TIME_UNITS; k++) {

                    int instanceIndex = (i * NUMBER_TIME_UNITS * TYPES_OF_INSTANCES.length) + (j * NUMBER_TIME_UNITS) + k;

                    if (timeBags[instanceIndex] != 0) {

                        bck[index] = (NUMBER_TIME_UNITS - k) * COST_PER_HOUR[j];
                        index++;
                    }
                }
            }
        }

        return bck;
    }

    private double[] getRetainingCost() {

        int index = 0;

        double[] brc = new double[getTotalNumOfCurrentTimeBags()];

        int[] timeBags = this.getTimeBags();

        for (int i = 0; i < this.getNumberOfRegions(); i++) {
            for (int j = 0; j < TYPES_OF_INSTANCES.length; j++) {
                for (int k = 0; k < NUMBER_TIME_UNITS; k++) {

                    int instanceIndex = (i * NUMBER_TIME_UNITS * TYPES_OF_INSTANCES.length) + (j * NUMBER_TIME_UNITS) + k;

                    if (timeBags[instanceIndex] != 0) {

                        brc[index] = k * COST_PER_HOUR[j];
                        index++;
                    }
                }
            }
        }

        return brc;
    }

    private int[] getMaxNumOfEachInstanceType() {
        int maxNumOfEachInstanceType[] = new int[this.getNumberOfRegions() * TYPES_OF_INSTANCES.length];
        int index = 0;

        for (int i = 0; i < this.getNumberOfRegions(); i++) {
            for (int j = 0; j < TYPES_OF_INSTANCES.length; j++) {
                maxNumOfEachInstanceType[index++] = MAX_NUMBER_OF_INSTANCES;
            }
        }

        return maxNumOfEachInstanceType;
    }

    private int[] getStartTimeOfEachInstanceType() {
        int startTimeOfEachInstanceType[] = new int[this.getNumberOfRegions() * TYPES_OF_INSTANCES.length];
        int index = 0;

        for (int i = 0; i < this.getNumberOfRegions(); i++) {
            for (int j = 0; j < TYPES_OF_INSTANCES.length; j++) {
                startTimeOfEachInstanceType[index++] = START_TIME;
            }
        }

        return startTimeOfEachInstanceType;
    }

    public int[] getTimeBags() {
        return timeBags;
    }

    public void setTimeBags(int[] timeBags) {
        this.timeBags = timeBags;
    }

    public int getNumberOfRegions() {
        return numberOfRegions;
    }

    public void setNumberOfRegions(int numberOfRegions) {
        this.numberOfRegions = numberOfRegions;
    }

    public CMLDeployer getCmlDeployer() {
        return CMLDeployer.getSingleton();
    }
}
