package br.ufrgs.inf.prosoft.adaptivecaching.analysis.decision.flowchart;

import br.ufrgs.inf.prosoft.adaptivecaching.analysis.decision.CacheDecider;
import br.ufrgs.inf.prosoft.adaptivecaching.analysis.decision.flowchart.model.MethodEntry;
import br.ufrgs.inf.prosoft.adaptivecaching.analysis.decision.flowchart.model.MethodStats;
import br.ufrgs.inf.prosoft.adaptivecaching.analysis.decision.flowchart.stats.CacheabilityMetrics;
import br.ufrgs.inf.prosoft.adaptivecaching.analysis.decision.flowchart.stats.CacheabilityPatternDecider;
import br.ufrgs.inf.prosoft.tigris.monitoring.metadata.LogTrace;
import br.ufrgs.inf.prosoft.tigris.monitoring.metadata.MethodInfo;
import br.ufrgs.inf.prosoft.adaptivecaching.cache.CacheInfo;
import org.apache.commons.lang3.builder.EqualsBuilder;
import org.apache.commons.math3.stat.descriptive.SummaryStatistics;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.PrintWriter;
import java.util.HashMap;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

/**
 * The type Flowchart work flow.
 */
public class FlowchartWorkFlow {

    /**
     * The Methods info map.
     */
    protected HashMap<MethodInfo, MethodStats> methodsInfoMap;
    /**
     * The Logger.
     */
    Logger logger = LoggerFactory.getLogger(FlowchartWorkFlow.class);

    private CacheDecider decider;
    private CacheInfo cacheInfo;

    /**
     * The Execution time stats.
     */
    SummaryStatistics executionTimeStats = new SummaryStatistics();
    /**
     * The Shareability stats.
     */
    SummaryStatistics shareabilityStats = new SummaryStatistics();
    /**
     * The Frequency stats.
     */
    SummaryStatistics frequencyStats = new SummaryStatistics();
    /**
     * The Miss stats.
     */
    SummaryStatistics missStats = new SummaryStatistics();
    /**
     * The Hit stats.
     */
    SummaryStatistics hitStats = new SummaryStatistics();

    /**
     * Instantiates a new Flowchart work flow.
     *
     * @param cacheInfo the cache info
     * @param logList   the log list
     */
    public FlowchartWorkFlow(CacheInfo cacheInfo, List<LogTrace> logList) {
        this.decider = new CacheabilityPatternDecider(logList.size(), this);
        this.cacheInfo = cacheInfo;
        this.methodsInfoMap = countOccurrences(logList);

        logger.debug(methodsInfoMap.size() + " unique method calls identified from "
                + logList.size() + " original traces");

        logger.debug("Average ExecutionTime: " + executionTimeStats.getMean());
        logger.debug("Average HitRatio: " + hitStats.getMean());
        logger.debug("Average MissRatio: " + missStats.getMean());
        logger.debug("Average shareability: " + shareabilityStats.getMean());
        logger.debug("Average frequency: " + frequencyStats.getMean());
        logger.debug("StdDv ExecutionTime: " + executionTimeStats.getStandardDeviation());
        logger.debug("StdDv HitRatio: " + hitStats.getStandardDeviation());
        logger.debug("StdDv MissRatio: " + missStats.getStandardDeviation());
        logger.debug("StdDv shareability: " + shareabilityStats.getStandardDeviation());
        logger.debug("StdDv frequency: " + frequencyStats.getStandardDeviation());

        int k = 0;
        logger.debug("Using " + k + " stdDev to calculate thresholds...");
        logger.debug("Threshold ExecutionTime: " + expensivenessThreshold(k));
        logger.debug("Threshold HitRatio: " + hitThreshold(k));
        logger.debug("Threshold MissRatio: " + missThreshold(k));
        logger.debug("Threshold Shareability: " + shareabilityThreshold(k));
        logger.debug("Threshold frequency: " + frequencyThreshold(k));
    }

    /**
     * Filter cacheable methods set.
     *
     * @param expiryTime the expiry time
     * @return the set
     */
    public Set<MethodEntry> filterCacheableMethods(long expiryTime) {
        logger.debug("Deciding if methods are cacheable...");

        Set<MethodEntry> cacheableMethods = getMethodsInfoMap().keySet().stream()
                .filter(mi -> decider.isCacheable(cacheInfo, mi, getMethodsInfoMap().get(mi)))
                .parallel().map(mi -> new MethodEntry(mi, getMethodsInfoMap().get(mi), System.currentTimeMillis() + expiryTime))
                .collect(Collectors.toSet());

        logger.info(cacheableMethods.size() + " cacheable methods detected. Printing files...");
        printAll(expiryTime, cacheableMethods);

        return cacheableMethods;
    }

    /**
     * Count occurrences hash map.
     *
     * @param logs the logs
     * @return the hash map
     */
    public HashMap<MethodInfo, MethodStats> countOccurrences(List<LogTrace> logs) {
        HashMap<MethodInfo, MethodStats> methodInfoMap = new HashMap<>();

        for (int i = 0; i < logs.size(); i++) {
            LogTrace logTrace = logs.get(i);

            if (methodInfoMap.containsKey(logTrace.getMethodInfo()))
                continue;

            MethodStats methodStats = new MethodStats(logTrace);

            for (int j = i+1; j < logs.size(); j++) {
                LogTrace traceCompare = logs.get(j);

                //if similar methods: same signature and params, different return
                if (traceCompare.getMethodInfo()
                        .equalsWithoutReturnedValue(logTrace.getMethodInfo())) {
                    //if identical methods
                    if (EqualsBuilder.reflectionEquals(
                            traceCompare.getMethodInfo().getReturnedValue(),
                            logTrace.getMethodInfo().getReturnedValue())) {
                        methodStats.addSameOccurrence(traceCompare);
                    }
                    else methodStats.addDifferentReturnOccurrence();
                }
            }

            methodInfoMap.put(logTrace.getMethodInfo(), methodStats);

            executionTimeStats.addValue(methodStats.getSameOccurrencesTotalExecutionTime());
            shareabilityStats.addValue(methodStats.shareability());
            frequencyStats.addValue(methodStats.getNumberOfSameOccurrences());
            missStats.addValue(methodStats.missRatio());
            hitStats.addValue(methodStats.hitRatio());
        }

        return methodInfoMap;
    }

    /**
     * Gets methods info map.
     *
     * @return the methods info map
     */
    public HashMap<MethodInfo, MethodStats> getMethodsInfoMap() {
        return methodsInfoMap;
    }

    /**
     * Hit threshold double.
     *
     * @param kStdDev the k std dev
     * @return the double
     */
//getting X% with most hits
    public double hitThreshold(int kStdDev) {
        return hitStats.getMean() + (kStdDev * hitStats.getStandardDeviation());
    }

    /**
     * Miss threshold double.
     *
     * @param kStdDev the k std dev
     * @return the double
     */
//getting X% with most misses
    public double missThreshold(int kStdDev) {
        return missStats.getMean() + (kStdDev * missStats.getStandardDeviation());
    }

    /**
     * Expensiveness threshold double.
     *
     * @param kStdDev the k std dev
     * @return the double
     */
//getting X% most expensive methods
    public double expensivenessThreshold(int kStdDev) {
        return executionTimeStats.getMean() + (kStdDev * executionTimeStats.getStandardDeviation());
    }

    /**
     * Shareability threshold double.
     *
     * @param kStdDev the k std dev
     * @return the double
     */
    public double shareabilityThreshold(int kStdDev) {
        return shareabilityStats.getMean() + (kStdDev * shareabilityStats.getStandardDeviation());
    }

    /**
     * Frequency threshold double.
     *
     * @param kStdDev the k std dev
     * @return the double
     */
//getting X% most frenquent
    public double frequencyThreshold(int kStdDev) {
        return frequencyStats.getMean() + (kStdDev * frequencyStats.getStandardDeviation());
    }

    /**
     * General miss ratio from a signature
     *
     * @param signature the signature
     * @return miss ratio
     */
    public double getMissRatio(String signature) {
        long occurrences = 0;
        long methods = 0;
        for (MethodInfo mi : methodsInfoMap.keySet()) {
            if (mi.getSignature().equals(signature)) {
                occurrences += methodsInfoMap.get(mi).getNumberOfSameOccurrences();
                methods++;
            }
        }
        return occurrences / methods;
    }

    /**
     * General hit ratio from a signature
     *
     * @param signature the signature
     * @return hit ratio
     */
    public double getHitRatio(String signature) {
        long occurrences = 0;
        long methods = 0;
        for (MethodInfo mi : methodsInfoMap.keySet()) {
            if (mi.getSignature().equals(signature)) {
                occurrences += methodsInfoMap.get(mi).getNumberOfSameOccurrences();
                methods++;
            }
        }
        return occurrences / methods;
    }

    private void printAll(long expiryTime, Set<MethodEntry> cacheableMethods) {
        //TODO remove: print all unique methods and metrics to csv file
        try {
            final PrintWriter pw = new PrintWriter(new File("allmethods.csv"));
            pw.write("isStaticData,changeMoreThanUsed,usedByManyRequests,isUserSpecific,isCacheSizeLarge,isDataSizeLarge,isExpensive,signature,numberOfSameOccurrences,numberOfDifferentReturnOccurrences,totalOccurrences,sameOccurrencesAverageExecutionTime,sameOccurrencesTotalExecutionTime,hitRatio,missRatio\n");
            getMethodsInfoMap().keySet().stream().forEach(mi -> pw.write(CacheabilityMetrics.allMetricsToString(cacheInfo, mi, getMethodsInfoMap().get(mi), this, getMethodsInfoMap().size()) + "," + new MethodEntry(mi, getMethodsInfoMap().get(mi), System.currentTimeMillis() + expiryTime).getMethodInfo().getSignature() + "," + new MethodEntry(mi, getMethodsInfoMap().get(mi), System.currentTimeMillis() + expiryTime).getMethodStats().toCSV() + '\n'));
            pw.close();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }

        //TODO remove: print cacheable methods to csv file
        try {
            final PrintWriter pw = new PrintWriter(new File("cacheablemethods.csv"));
            pw.write("isStaticData,changeMoreThanUsed,usedByManyRequests,isUserSpecific,isCacheSizeLarge,isDataSizeLarge,isExpensive,signature,numberOfSameOccurrences,numberOfDifferentReturnOccurrences,totalOccurrences,sameOccurrencesAverageExecutionTime,sameOccurrencesTotalExecutionTime,hitRatio,missRatio\n");
            cacheableMethods.stream().forEach(ma -> pw.write(CacheabilityMetrics.allMetricsToString(cacheInfo, ma.getMethodInfo(), ma.getMethodStats(), this, getMethodsInfoMap().size()) + "," + ma.getMethodInfo().getSignature() + "," + ma.getMethodStats().toCSV() + '\n'));
            pw.close();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }
    }
}
