package de.wwwu.awolf.presenter.evaluation;

import de.wwwu.awolf.model.Line;
import de.wwwu.awolf.model.LineModel;
import de.wwwu.awolf.model.communication.Data;
import de.wwwu.awolf.model.communication.EvaluationData;
import de.wwwu.awolf.model.communication.SubscriberType;
import de.wwwu.awolf.model.evaluation.ComparisonResult;
import de.wwwu.awolf.presenter.Presenter;
import de.wwwu.awolf.presenter.algorithms.Algorithm;
import de.wwwu.awolf.presenter.data.DataHandler;
import de.wwwu.awolf.presenter.data.generator.DatasetGenerator;
import de.wwwu.awolf.presenter.evaluation.measures.PercentageErrorBasedMeasure;
import de.wwwu.awolf.presenter.evaluation.measures.ScaleDependentMeasure;
import de.wwwu.awolf.presenter.evaluation.measures.ScaledErrorBasedMeasure;
import de.wwwu.awolf.presenter.util.Logging;

import java.io.File;
import java.util.*;
import java.util.concurrent.Flow;

/**
 * Implementierung verschiedener Algorithmen zur Berechnung von Ausgleichsgeraden.
 *
 * @Author: Armin Wolf
 * @Email: a_wolf28@uni-muenster.de
 * @Date: 01.08.2017.
 */
public class EvaluatationHandler implements Runnable, Flow.Publisher<Data> {

    private LineModel arrangement;
    private DatasetGenerator generator;

    //übergebene Parameter
    private int type;
    private int iterations;
    private int alg;
    private Flow.Subscriber<? super Data> subscriber;
    private Map<Algorithm.Type, Map<String, String>> resultMapping;

    /**
     * Konstruktor zur evaluation
     *
     * @param type       Typ der evaluation
     * @param n          Größe des Datensatzes
     * @param alg        0 = lms,
     *                   1 = rm,
     *                   2 = ts,
     *                   3 = lms, rm,
     *                   4 = lms, ts,
     *                   5 = rm,  ts,
     *                   6 = lms, rm, ts,
     * @param datasettyp typ der zu generierenden Datensatz
     */
    public EvaluatationHandler(int type, int n, int alg, DataHandler.DataType datasettyp) {
        Presenter instance = Presenter.getInstance();
        subscribe(instance);
        this.arrangement = new LineModel();
        this.resultMapping = new EnumMap<>(Algorithm.Type.class);

        Set<Line> data = instance.generateDataset(n, datasettyp);
        Logging.logInfo("Starting the Benchmark...");
        arrangement.setLines(data);

        Logging.logInfo("Benchmark on Dataset: " + datasettyp + " with " + n + " points");

        this.type = type;
        this.iterations = n;
        this.alg = alg;
    }

    /**
     * Konstruktor zur evaluation
     *
     * @param type Typ der evaluation
     * @param alg  0 = lms,
     *             1 = rm,
     *             2 = ts,
     *             3 = lms, rm,
     *             4 = lms, ts,
     *             5 = rm,  ts,
     *             6 = lms, rm, ts,
     * @param file Datei die importiert werden soll
     */
    public EvaluatationHandler(int type, int alg, File file) {
        Presenter instance = Presenter.getInstance();
        subscribe(instance);
        instance.importDataset(file);
        this.arrangement = instance.getModel();
        this.resultMapping = new EnumMap<>(Algorithm.Type.class);

        this.type = type;
        this.alg = alg;
    }

    private Map<Algorithm.Type, Map<String, String>> benchmarkSameEstimator(final Algorithm.Type advanced, final Algorithm.Type naiv) {
        Logging.logInfo("AlgorithmComparison with Types: " + advanced.name() + ", " + naiv.name());
        AlgorithmComparison comparison = new AlgorithmComparison(Arrays.asList(naiv, advanced), Collections.unmodifiableSet(arrangement.getLines()));
        ComparisonResult comparisonResult = comparison.compare();


        Map<String, String> result = new HashMap<>();
        result.putAll(getScaleDependentMeasure(arrangement.getLines(), comparisonResult, advanced));
        result.putAll(getScaledErrorBasedMeasure(arrangement.getLines(), comparisonResult, advanced, naiv));
        Logging.logInfo("finished with execution of the algorithms.");

        this.resultMapping.put(advanced, result);
        return this.resultMapping;
    }

    private Map<Algorithm.Type, Map<String, String>> benchmarkDifferentEstimators(List<Algorithm.Type> types) {
        Logging.logInfo("AlgorithmComparison with Types: " + types);
        AlgorithmComparison comparison = new AlgorithmComparison(types, Collections.unmodifiableSet(arrangement.getLines()));
        ComparisonResult comparisonResult = comparison.compare();

        types.forEach(typeEntry -> this.resultMapping.put(typeEntry, getPercentigeErrorBasedMeasure(arrangement.getLines(), comparisonResult, typeEntry)));
        Logging.logInfo("finished with execution of the algorithms.");
        return this.resultMapping;
    }


    /**
     * Startet die Evaluation zu den passenden Typ. Bei beendigung wird der Beobachter informiert.
     */
    @Override
    public void run() {
        Map<Algorithm.Type, Map<String, String>> result = new EnumMap<>(Algorithm.Type.class);
        switch (type) {
            case 0:
                //der alg der gewählt wurde
                if (alg == 0) {
                    result = benchmarkSameEstimator(Algorithm.Type.LMS, Algorithm.Type.NAIV_LMS);
                } else if (alg == 1) {
                    result = benchmarkSameEstimator(Algorithm.Type.RM, Algorithm.Type.NAIV_RM);
                } else {
                    result = benchmarkSameEstimator(Algorithm.Type.TS, Algorithm.Type.NAIV_TS);
                }
                break;
            case 1:
                switch (alg) {
                    case 3:
                        result = benchmarkDifferentEstimators(Arrays.asList(Algorithm.Type.LMS, Algorithm.Type.RM));
                        break;
                    case 4:
                        result = benchmarkDifferentEstimators(Arrays.asList(Algorithm.Type.LMS, Algorithm.Type.TS));
                        break;
                    case 5:
                        result = benchmarkDifferentEstimators(Arrays.asList(Algorithm.Type.RM, Algorithm.Type.TS));
                        break;
                    case 6:
                        result = benchmarkDifferentEstimators(Arrays.asList(Algorithm.Type.LMS, Algorithm.Type.RM, Algorithm.Type.TS));
                        break;
                }
                break;
        }
        sendTableApproximationData(result);
    }

    /**
     * Die berechneten Ergebnisse werden an den Beobachter übermittelt um dann visualisiert zu werden.
     *
     * @param result Ergebnisse
     */
    private void sendTableApproximationData(Map<Algorithm.Type, Map<String, String>> result) {

        EvaluationData data = new EvaluationData();
        data.setType(SubscriberType.EVALUATION_TABLE_DATA);
        data.setMultipleColumnResult(result);
        data.setRowsPerColumn(result.keySet().size());
        this.subscriber.onNext(data);
    }

    /**
     * Startet die Berechnung der skalierungsabbhängigen Maße.
     *
     * @param lines Liste der Geraden
     * @return Liste mit den Ergebnissen, bereit zum visualisieren
     */
    private Map<String, String> getScaleDependentMeasure(final Set<Line> lines, final ComparisonResult comparisonResult, final Algorithm.Type type) {

        Logging.logInfo("Calculating ScaleDependentMeasure for " + type);

        Double m = comparisonResult.get(type).getM();
        Double b = comparisonResult.get(type).getB();

        ScaleDependentMeasure scaleDependentMeasure = new ScaleDependentMeasure(lines, m, b);
        Map<String, String> ret = new HashMap<>();
        ret.put(type + " MSE", scaleDependentMeasure.mse().toString());
        ret.put(type + " RMSE", scaleDependentMeasure.rmse().toString());
        ret.put(type + " MAE", scaleDependentMeasure.mae().toString());
        ret.put(type + " MDAE", scaleDependentMeasure.mdae().toString());
        ret.put(type + " SLOPE", m.toString());
        ret.put(type + " y-INTERCEPTION", b.toString());

        Logging.logInfo("finished calculating ScaleDependentMeasure.");

        return ret;
    }

    /**
     * Startet die Berechnung der Maße die auf dem prozentualen Fehler basieren.
     *
     * @param lines Liste der Geraden
     * @return Liste mit den Ergebnissen, bereit zum visualisieren
     */
    private Map<String, String> getPercentigeErrorBasedMeasure(final Set<Line> lines, final ComparisonResult comparisonResult, final Algorithm.Type type) {
        Logging.logInfo("Calculating PercentigeErrorBasedMeasure for " + type);
        Double m = comparisonResult.get(type).getM();
        Double b = comparisonResult.get(type).getB();

        PercentageErrorBasedMeasure percentageErrorBasedMeasure = new PercentageErrorBasedMeasure(lines, m, b);
        Map<String, String> ret = new HashMap<>();
        ret.put(type + " MAPE", percentageErrorBasedMeasure.mape().toString());
        ret.put(type + " MDAPE", percentageErrorBasedMeasure.mdape().toString());
        ret.put(type + " RMSPE", percentageErrorBasedMeasure.rmspe().toString());
        ret.put(type + " RMDSPE", percentageErrorBasedMeasure.rmdspe().toString());
        ret.put(type + " SLOPE", m.toString());
        ret.put(type + " y-INTERCEPTION", b.toString());
        Logging.logInfo("finished calculating PercentigeErrorBasedMeasure.");
        return ret;
    }

    /**
     * Startet die Berechnung der skalierungsunabbhängigen Maße.
     *
     * @param lines Liste der Geraden
     * @return Liste mit den Ergebnissen, bereit zum visualisieren
     */
    private Map<String, String> getScaledErrorBasedMeasure(final Set<Line> lines, final ComparisonResult comparisonResult, final Algorithm.Type advanced, final Algorithm.Type naiv) {

        Logging.logInfo("Calculating ScaledErrorBasedMeasure for " + advanced + ", " + naiv);

        //first
        Double m = comparisonResult.get(advanced).getM();
        Double b = comparisonResult.get(advanced).getB();
        //second
        Double naivM = comparisonResult.get(naiv).getM();
        Double naivB = comparisonResult.get(naiv).getB();

        ScaledErrorBasedMeasure scaledErrorBasedMeasure = new ScaledErrorBasedMeasure(lines, m, b, naivM, naivB);
        Map<String, String> ret = new HashMap<>();
        ret.put(advanced + " MSE", scaledErrorBasedMeasure.mse().toString());
        ret.put(advanced + " RMSE", scaledErrorBasedMeasure.rmse().toString());
        ret.put(advanced + " MAE", scaledErrorBasedMeasure.mae().toString());
        ret.put(advanced + " MDAE", scaledErrorBasedMeasure.mdae().toString());
        ret.put(advanced + " Naiv-SLOPE", naivM.toString());
        ret.put(advanced + " Naiv-y-INTERCEPTION", naivB.toString());

        Logging.logInfo("finished calculating ScaledErrorBasedMeasure.");
        return ret;
    }

    /**
     * @return Liste der Geraden auf der die Berechnungen ausgeführt wurden
     */
    public Set<Line> getData() {
        return arrangement.getLines();
    }

    @Override
    public void subscribe(Flow.Subscriber<? super Data> subscriber) {
        this.subscriber = subscriber;
    }
}
