package cz.muni.disa.adamiss.se.stats.objects;

import cz.muni.disa.adamiss.se.common.Algo;
import javafx.util.Pair;

import java.io.FileWriter;
import java.io.IOException;
import java.util.*;
import java.util.stream.Collectors;

public record ExperimentReport(String name, DBReport dbStats, int threshold, double time, Algo algorithm) {

    public static String getHeader() {
        return "name, " +
                DBReport.getHeader() + ", " +
                "threshold, " +
                "time, " +
                "algorithm";
    }

    public static String toCSVWithHeader(List<ExperimentReport> reports) {
        StringBuilder eReportCSV = new StringBuilder(getHeader()).append(System.lineSeparator());
        reports.sort(Comparator.comparing(ExperimentReport::algorithm));
        Algo lastAlgorithm = null;
        for (ExperimentReport report : reports) {
            if (lastAlgorithm != null && report.algorithm() != lastAlgorithm) {
                eReportCSV.append(System.lineSeparator()).append(System.lineSeparator());
            }
            lastAlgorithm = report.algorithm();
            eReportCSV.append(report.toCSV()).append(System.lineSeparator());
        }
        return eReportCSV.toString();
    }

    public static void generateGNUPlotFiles(List<ExperimentReport> reports) {

        Map<Algo, List<ExperimentReport>> reportMap = reports.stream().collect(Collectors.groupingBy(ExperimentReport::algorithm));

        Map<Algo, List<Pair<Integer, Double>>> sizeMap = new HashMap<>();
        Map<Algo, List<Pair<Integer, Double>>> uniqueMap = new HashMap<>();
        Map<Algo, List<Pair<Integer, Double>>> minLengthMap = new HashMap<>();
        Map<Algo, List<Pair<Double, Double>>> avgLengthMap = new HashMap<>();
        Map<Algo, List<Pair<Integer, Double>>> maxLengthMap = new HashMap<>();
        Map<Algo, List<Pair<Integer, Double>>> minRepeatibilityMap = new HashMap<>();
        Map<Algo, List<Pair<Double, Double>>> avgRepeatibilityMap = new HashMap<>();
        Map<Algo, List<Pair<Integer, Double>>> maxRepeatibilityMap = new HashMap<>();
        Map<Algo, List<Pair<Double, Double>>> avgRepeatedMap = new HashMap<>();
        Map<Algo, List<Pair<Integer, Double>>> thresholdMap = new HashMap<>();

        for (Algo algo : reportMap.keySet()) {

            sizeMap.put(algo, new ArrayList<>());
            uniqueMap.put(algo, new ArrayList<>());
            minLengthMap.put(algo, new ArrayList<>());
            avgLengthMap.put(algo, new ArrayList<>());
            maxLengthMap.put(algo, new ArrayList<>());
            minRepeatibilityMap.put(algo, new ArrayList<>());
            avgRepeatibilityMap.put(algo, new ArrayList<>());
            maxRepeatibilityMap.put(algo, new ArrayList<>());
            avgRepeatedMap.put(algo, new ArrayList<>());
            thresholdMap.put(algo, new ArrayList<>());
            for (ExperimentReport report : reportMap.get(algo)) {
                sizeMap.get(algo).add(new Pair<>(report.dbStats().size(), report.time()));
                uniqueMap.get(algo).add(new Pair<>(report.dbStats().uniqueItems(), report.time()));
                minLengthMap.get(algo).add(new Pair<>(report.dbStats().minLength(), report.time()));
                avgLengthMap.get(algo).add(new Pair<>(report.dbStats().avgLength(), report.time()));
                maxLengthMap.get(algo).add(new Pair<>(report.dbStats().maxLength(), report.time()));
                minRepeatibilityMap.get(algo).add(new Pair<>(report.dbStats().minRepeatibility(), report.time()));
                avgRepeatibilityMap.get(algo).add(new Pair<>(report.dbStats().avgRepeatibility(), report.time()));
                maxRepeatibilityMap.get(algo).add(new Pair<>(report.dbStats().maxRepeatibility(), report.time()));
                avgRepeatedMap.get(algo).add(new Pair<>(report.dbStats().avgRepeatedItems(), report.time()));
                thresholdMap.get(algo).add(new Pair<>(report.threshold(), report.time()));
            }
        }
            mapSort(sizeMap);
            mapSort(uniqueMap);
            mapSort(minLengthMap);
            mapSort(avgLengthMap);
            mapSort(maxLengthMap);
            mapSort(minRepeatibilityMap);
            mapSort(avgRepeatibilityMap);
            mapSort(maxRepeatibilityMap);
            mapSort(avgRepeatedMap);
            mapSort(thresholdMap);

            mapPrint("size", sizeMap);
            mapPrint("unique", uniqueMap);
            mapPrint("minLength", minLengthMap);
            mapPrint("avgLength", avgLengthMap);
            mapPrint("maxLength", maxLengthMap);
            mapPrint("minRepeatibility", minRepeatibilityMap);
            mapPrint("avgRepeatibility", avgRepeatibilityMap);
            mapPrint("maxRepeatibility", maxRepeatibilityMap);
            mapPrint("avgRepeated", avgRepeatedMap);
            mapPrint("threshold", thresholdMap);
    }

    private static <K extends Comparable,V> void mapSort(Map<Algo, List<Pair<K,V>>> map){
        for (Algo algo : map.keySet()){
            map.get(algo).sort(Comparator.comparing(Pair::getKey));
        }
    }
    private static <K extends Comparable,V extends Comparable> void mapPrint(String name, Map<Algo, List<Pair<K,V>>> map){
        StringBuilder csvHeader = new StringBuilder(name);
        List<Algo> algoOrder = new ArrayList<>(map.keySet());
        algoOrder.sort((algo, t1) -> algo.name().compareTo(t1.name()));
        for (Algo algo : algoOrder){
            csvHeader.append(", ").append(CandleValues.header(algo.name()));
        }
        csvHeader.append(System.lineSeparator());

        Map<K, List<Pair<Algo,List<V>>>> transposedMap = new HashMap<>();
        Set<K> uniqueK = new HashSet<>();
        for(var a : map.values()){
            for (var p : a)
                uniqueK.add(p.getKey());
        }
        List<K> listUniqueK = new ArrayList<>(uniqueK);
        listUniqueK.sort(K::compareTo);
        for (K key: listUniqueK) {
            transposedMap.put(key, new ArrayList<>());
            for(Algo algo : algoOrder){

                transposedMap.get(key).add(
                        new Pair<Algo,List<V>>(
                                algo,
                                map.get(algo).stream()
                                        .filter((x)->x.getKey().equals(key))
                                        .map(x->x.getValue())
                                        .collect(Collectors.toList())
                        )
                );
            }

        }

        StringBuilder valuesCSV = new StringBuilder(csvHeader);
        List<K> keyOrder = new ArrayList<>(transposedMap.keySet());
        keyOrder = keyOrder.stream().sorted().collect(Collectors.toList());
        for (K key : keyOrder){
            valuesCSV.append(key.toString()).append(", ");
            var pairList = transposedMap.get(key);
            for (var algo : algoOrder){
                //todo:compute candle value and average
                var times = pairList.stream()
                        .filter((p)->p.getKey().equals(algo))
                        .map(x->x.getValue())
                        .flatMap(List::stream)
                        .collect(Collectors.toList());
                times.sort(V::compareTo);
                int indexQuarter = (int)Math.round(times.size()/4d);
                var min = times.get(0);
                var firstQuartile = times.get(Math.max(0,indexQuarter*1-1));
                var median = times.get(Math.max(0, times.size()/2-1));
                var avg = times.stream().mapToDouble(v -> (double)v).average().getAsDouble();
                var thirdQuartile = times.get(Math.max(0,times.size()-indexQuarter-1));
                var max = times.get(times.size()-1);
                var timeValues = new CandleValues<V>(algo.name(), min, firstQuartile, median, avg, thirdQuartile, max);
                valuesCSV.append(timeValues.values());
                valuesCSV.append(", ");
            }
            valuesCSV.append(System.lineSeparator());
        }

        saveGnuCSVFile(valuesCSV.toString(), name);
    }

    public static void saveGnuCSVFile(String data, String name) {
        try {
            FileWriter report = new FileWriter(name + ".csv");
            report.write(data);
            report.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public String toCSV() {
        return this.name() + "," +
                dbStats().toCSV() + "," +
                this.threshold() + "," +
                this.time() + "," +
                this.algorithm().name();
    }
}


record CandleValues<T> (String name, T min, T firstQuartile, T median, double avg, T thirdQuartile, T max){

    public static String header(String name){
        StringBuilder builder = new StringBuilder();
        builder.append(name).append("_min, ");
        builder.append(name).append("_firstQuartile, ");
        builder.append(name).append("_median, ");
        builder.append(name).append("_avg, ");
        builder.append(name).append("_thirdQuartile, ");
        builder.append(name).append("_max");
        return builder.toString();
    }

    public String values(){
        StringBuilder builder = new StringBuilder();
        builder.append(min()).append(", ");
        builder.append(firstQuartile()).append(", ");
        builder.append(median()).append(", ");
        builder.append(avg()).append(", ");
        builder.append(thirdQuartile()).append(", ");
        builder.append(max());
        return builder.toString();
    }
}
