package br.ufrgs.inf.prosoft.tigris.metrics;

import br.ufrgs.inf.prosoft.tigris.utils.StatisticalTest;
import org.apache.commons.math3.stat.descriptive.DescriptiveStatistics;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Collection;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentSkipListSet;

/**
 * The type Data filtering.
 */
public class DataFiltering {

    static Logger logger = LoggerFactory.getLogger(DataFiltering.class);

    private static final DescriptiveStatistics frequencies = new DescriptiveStatistics();
    private static final DescriptiveStatistics changeabilities = new DescriptiveStatistics();
    private static final DescriptiveStatistics errorprones = new DescriptiveStatistics();
    private static final DescriptiveStatistics latencies = new DescriptiveStatistics();
    private static final DescriptiveStatistics maintainabilities = new DescriptiveStatistics();
    private static final DescriptiveStatistics userBehaviors = new DescriptiveStatistics();
    private static final DescriptiveStatistics concurrencies = new DescriptiveStatistics();
    private static final DescriptiveStatistics expensiveness = new DescriptiveStatistics();
    private static final DescriptiveStatistics globalimpacts = new DescriptiveStatistics();

    public static Set<String> calculate(StaticMetrics staticMetrics, Map<String, LightweightMetrics> metrics){
        frequencies.clear();
        changeabilities.clear();
        errorprones.clear();
        latencies.clear();
        maintainabilities.clear();
        userBehaviors.clear();
        concurrencies.clear();
        expensiveness.clear();
        globalimpacts.clear();

        logger.info("Indexing {} events", metrics.size());
        for (Map.Entry<String, LightweightMetrics> method : metrics.entrySet()) {
            logger.debug("Indexing {}...", method.getKey());
            LightweightMetrics sc = method.getValue();
            frequencies.addValue(sc.getFrequency());
            changeabilities.addValue(sc.getChangeability());
            errorprones.addValue(sc.getErrorprone());
            latencies.addValue(sc.getLatency());
            maintainabilities.addValue(sc.getMaintainability());
            userBehaviors.addValue(sc.getUserBehavior());
            concurrencies.addValue(sc.getConcurrency());
            expensiveness.addValue(sc.getExpensiveness());
            globalimpacts.addValue(sc.getGlobalImpact());
        }

        logger.debug("Metric: frequency; Normal Distribution: " + isNormalDistribution(frequencies));
        logger.debug("Metric: maintainability; Normal Distribution: " + isNormalDistribution(maintainabilities));
        logger.debug("Metric: changeability; Normal Distribution: " + isNormalDistribution(changeabilities));
        logger.debug("Metric: userBehavior; Normal Distribution: " + isNormalDistribution(userBehaviors));
        logger.debug("Metric: concurrency; Normal Distribution: " + isNormalDistribution(concurrencies));
        logger.debug("Metric: errorprone; Normal Distribution: " + isNormalDistribution(errorprones));
        logger.debug("Metric: expensiveness; Normal Distribution: " + isNormalDistribution(expensiveness));
        logger.debug("Metric: globalimpact; Normal Distribution: " + isNormalDistribution(globalimpacts));
        logger.debug("Metric: latency; Normal Distribution: " + isNormalDistribution(latencies));

        try {
            final PrintWriter pw = new PrintWriter(new File("lightweightanalysis.csv"));
            pw.write("method,frequency,maintainability,changeability,userBehavior,concurrency,errorprone,expensiveness,globalimpact,latency,vfrequency,vmaintainability,vchangeability,vuserBehavior,vconcurrency,verrorprone,vexpensiveness,vglobalimpact,vlatency\n");
            for (String name : metrics.keySet()) {
                LightweightMetrics lm = metrics.get(name);
                StaticMetrics.StaticMetric staticMetric = staticMetrics.getMetrics(lm.getName());
                if (staticMetric == null)
                    continue;
                pw.write(metrics.get(name).getName() + "," +
                        getGroup(frequencies, metrics.get(name).getFrequency()) +
                        "," + getGroup(maintainabilities, staticMetric.cyclomatic) +
                        "," + getGroup(changeabilities, lm.getChangeability()) +
                        "," + getGroup(userBehaviors, lm.getUserBehavior()) +
                        "," + getGroup(concurrencies, lm.getConcurrency()) +
                        "," + getGroup(errorprones, staticMetric.maxNesting) +
                        "," + getGroup(expensiveness, lm.getExpensiveness()) +
                        "," + getGroup(globalimpacts, staticMetric.countOutput) +
                        "," + getGroup(latencies, metrics.get(name).getLatency()) +
                        "," +
                        allMetricsToString(lm, staticMetrics) + '\n');
            }
            pw.close();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }

        Set<String> relevantEvents = new ConcurrentSkipListSet<>();

        logger.info("Calculating relevance for {} events", metrics.size());
        for (Map.Entry<String, LightweightMetrics> method : metrics.entrySet()) {
            logger.debug("Calculating relevance for {}...", method.getKey());
            if (extendedFilter(method.getValue())) {
                relevantEvents.add(method.getKey());
            }
        }

        return relevantEvents;
    }

    private static boolean isNormalDistribution(DescriptiveStatistics stats){
        return StatisticalTest.isNormalDistribution(stats.getValues(), 0.05);
    }

    public static boolean extendedFilter(LightweightMetrics methodMetrics) {
        return
                //extended filter: less changeable C more frequent C (more user_behavior U (less user_behavior C more expensive))
                ((isGroup("less", changeabilities, methodMetrics.getChangeability())
                        || isGroup("normal", changeabilities, methodMetrics.getChangeability()))
                        && (isGroup("more", frequencies, methodMetrics.getFrequency())
                        || isGroup("normal", frequencies, methodMetrics.getFrequency()))
                        && ((isGroup("more", userBehaviors, methodMetrics.getUserBehavior())
                        || isGroup("normal", userBehaviors, methodMetrics.getUserBehavior()))
                        || ((isGroup("less", userBehaviors, methodMetrics.getUserBehavior())
                        || isGroup("normal", userBehaviors, methodMetrics.getUserBehavior()))
                        && (isGroup("normal", expensiveness, methodMetrics.getExpensiveness())
                        || isGroup("more", expensiveness, methodMetrics.getExpensiveness())))));
    }

    public static boolean restrictedFilter(LightweightMetrics methodMetrics) {
        return

        //restricted filter: less changeable C more frequent C (more user_behavior U (less user_behavior C more expensive))
            (isGroup("less", changeabilities, methodMetrics.getChangeability())
                && isGroup("more", frequencies, methodMetrics.getFrequency())
                && (isGroup("more", userBehaviors, methodMetrics.getUserBehavior())
                    || (isGroup("less", userBehaviors, methodMetrics.getUserBehavior())
                        && isGroup("more", expensiveness, methodMetrics.getExpensiveness()))));

//            if (isGroup("least", changeabilities, methodMetrics.getChangeability())
//                && isGroup("most", frequencies, methodMetrics.getFrequency())
//                && (isGroup("most", userBehaviors, methodMetrics.getUserBehavior())
//                    || (isGroup("least", userBehaviors, methodMetrics.getUserBehavior())
//                        && isGroup("most", expensiveness, methodMetrics.getExpensiveness()))))
//
//            if (isGroup("less", changeabilities, methodMetrics.getChangeability())
//                    && isGroup("most", frequencies, methodMetrics.getFrequency())
//                    && isGroup("most", expensiveness, methodMetrics.getExpensiveness()));
    }

    /**
     * Is group boolean.
     *
     * @param group    the group
     * @param criteria the criteria
     * @param value    the value
     * @return the boolean
     */
    public static boolean isGroup(String group, DescriptiveStatistics criteria, double value) {

        boolean isNormal = isNormalDistribution(criteria);
        DescriptiveStatistics half = new DescriptiveStatistics();
        if (!isNormal && (group.equals("most") || group.equals("least"))) {
            for (double val : criteria.getValues())
                if (group.equals("most") && val >= criteria.getPercentile(75))
                    half.addValue(val);
                else if (group.equals("least") && val <= criteria.getPercentile(25))
                    half.addValue(val);
        }

        switch (group) {
            case "normal":
                if (isNormal)
                    return (value > (criteria.getMean() - criteria.getStandardDeviation()) &&
                            value < (criteria.getMean() + criteria.getStandardDeviation()));
                else
                    return (value > criteria.getPercentile(25) &&
                            value < criteria.getPercentile(75));
            case "most":
                if (isNormal)
                    return value >= (criteria.getMean() + (2 * criteria.getStandardDeviation()));
                else
                    return value >= half.getPercentile(50);
            case "least":
                if (isNormal)
                    return value <= (criteria.getMean() + (2 * criteria.getStandardDeviation()));
                else
                    return value <= half.getPercentile(50);
            case "more":
                if (isNormal)
                    return value >= (criteria.getMean() + criteria.getStandardDeviation());
                else
                    return value >= criteria.getPercentile(75);
            case "less":
                if (isNormal)
                    return value <= (criteria.getMean() - criteria.getStandardDeviation());
                else
                    return value <= criteria.getPercentile(25);
        }

        throw new RuntimeException("Não foi possível classificar...");
    }

    /**
     * Get group string.
     *
     * @param criteria the criteria
     * @param value    the value
     * @return the string
     */
    public static String getGroup(DescriptiveStatistics criteria, double value) {

        boolean isNormal = isNormalDistribution(criteria);
        DescriptiveStatistics upperHalf = new DescriptiveStatistics();
        DescriptiveStatistics lowerHalf = new DescriptiveStatistics();
        if (!isNormal) {
            for (double val : criteria.getValues())
                if (val >= criteria.getPercentile(75))
                    upperHalf.addValue(val);
                else if (val <= criteria.getPercentile(25))
                    lowerHalf.addValue(val);
        }

        if (isNormal) {
            if (value > (criteria.getMean() - criteria.getStandardDeviation()) &&
                    value < (criteria.getMean() + criteria.getStandardDeviation()))
                return "normal";
            else if (value >= (criteria.getMean() + (2 * criteria.getStandardDeviation())))
                return "most";
            else if (value <= (criteria.getMean() + (2 * criteria.getStandardDeviation())))
                return "least";
            else if (value >= (criteria.getMean() + criteria.getStandardDeviation()))
                return "more";
            else if (value <= (criteria.getMean() - criteria.getStandardDeviation()))
                return "less";
        } else {
            if (value > criteria.getPercentile(25) &&
                    value < criteria.getPercentile(75))
                return "normal";
            else if (value >= upperHalf.getPercentile(50))
                return "most";
            else if (value <= lowerHalf.getPercentile(50))
                return "least";
            else if (value >= criteria.getPercentile(75))
                return "more";
            else if (value <= criteria.getPercentile(25))
                return "less";
        }

        throw new RuntimeException("Não foi possível classificar...");
    }

    /**
     * All metrics to string string.
     *
     * @param lightweightMetrics the lightweight metrics
     * @param staticMetrics      the static metrics
     * @return the string
     */
    public static String allMetricsToString(LightweightMetrics lightweightMetrics, StaticMetrics staticMetrics) {
        //frequency,maintainability,changeability,userBehavior,concurrency,errorprone,expensiveness,globalimpact,latency
        return lightweightMetrics.getFrequency() +
                "," + staticMetrics.getMetrics(lightweightMetrics.getName()).cyclomatic +
                "," + lightweightMetrics.getChangeability() +
                "," + lightweightMetrics.getUserBehavior() +
                "," + lightweightMetrics.getConcurrency() +
                "," + staticMetrics.getMetrics(lightweightMetrics.getName()).maxNesting +
                "," + lightweightMetrics.getExpensiveness() +
                "," + staticMetrics.getMetrics(lightweightMetrics.getName()).countOutput +
                "," + lightweightMetrics.getLatency();
    }
}
