package de.wwwu.awolf.presenter.algorithms.naiv;

import de.wwwu.awolf.model.Line;
import de.wwwu.awolf.model.Point;
import de.wwwu.awolf.model.communication.Data;
import de.wwwu.awolf.presenter.AbstractPresenter;
import de.wwwu.awolf.presenter.algorithms.Algorithm;
import de.wwwu.awolf.presenter.util.FastElementSelector;
import de.wwwu.awolf.presenter.util.Logging;

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: 15.09.2017.
 */
public class NaivRepeatedMedianEstimator implements Algorithm {

    private static final Algorithm.Type type = Type.NAIV_RM;

    private List<Line> setOfLines;
    private Map<String, List<Double>> slopesPerLine;
    private Map<String, List<Double>> interceptPerLine;
    private List<Double> xMedians;
    private List<Double> yMedians;
    private double medianX;
    private double medianY;
    private Flow.Subscriber<? super Data> subscriber;
    private AbstractPresenter presenter;

    @Override
    public Line call() {

        slopesPerLine = new HashMap<>();
        interceptPerLine = new HashMap<>();
        xMedians = new ArrayList<>();
        yMedians = new ArrayList<>();

        //init the List for the slopes
        Logging.logInfo("=== S T A R T - naiv R M ===");
        long start;
        long end;
        start = System.currentTimeMillis();
        for (Line leq : setOfLines) {
            slopesPerLine.computeIfAbsent(leq.getId(), k -> new ArrayList<>());
            interceptPerLine.computeIfAbsent(leq.getId(), k -> new ArrayList<>());
        }

        //calculate all slopes for each line
        Point ret;
        for (int i = 0; i < setOfLines.size(); i++) {
            for (int j = i + 1; j < setOfLines.size(); j++) {
                ret = calculateLine(setOfLines.get(i), setOfLines.get(j));
                slopesPerLine.get(setOfLines.get(i).getId()).add(ret.getX());
                interceptPerLine.get(setOfLines.get(i).getId()).add(ret.getY());
            }
        }

        //berechne mediane Steigung
        for (String l : slopesPerLine.keySet()) {
            List<Double> list = slopesPerLine.get(l);
            int size = list.size() / 2;
            if (size > 0) {
                double medianX = FastElementSelector.randomizedSelect(list, size);
                xMedians.add(medianX);
            }
        }

        //berechne medianen y-Achsenabschnitt
        for (String l : interceptPerLine.keySet()) {
            List<Double> list = interceptPerLine.get(l);
            int size = list.size() / 2;
            if (size > 0) {
                double medianY = FastElementSelector.randomizedSelect(list, size);
                yMedians.add(medianY);
            }
        }

        medianX = FastElementSelector.randomizedSelect(xMedians, xMedians.size() * 0.5);
        medianY = FastElementSelector.randomizedSelect(yMedians, yMedians.size() * 0.5);
        end = System.currentTimeMillis();
        Logging.logInfo("=== E N D - naiv R M ===");
        Logging.logInfo("Slope: "  + getSlope() + ", y-Interception: " + getYInterception());

        return new Line(getSlope(), getYInterception());
    }

    @Override
    public void setInput(Set<Line> lines) {
        this.setOfLines = new ArrayList<>(lines);
    }

    @Override
    public Type getType() {
        return type;
    }

    @Override
    public void setPresenter(AbstractPresenter presenter) {
        this.presenter = presenter;
        subscribe(presenter);
    }

    /**
     * Berechnet die  Geraden zwischen zwei Punkten im dualen Raum.
     *
     * @param startPoint Gerade 1 => Startpunkt mit den Koordianten (m,b)
     * @param endPoint   Gerade 2 => Endpunkt mit den Koordianten (m', b')
     * @return
     */
    private Point calculateLine(Line startPoint, Line endPoint) {
        double xi;
        double xj;
        double yi;
        double yj;

        if (endPoint.getM() > startPoint.getM()) {
            xi = startPoint.getM();
            yi = startPoint.getB();
            xj = endPoint.getM();
            yj = endPoint.getB();
        } else {
            xj = startPoint.getM();
            yj = startPoint.getB();
            xi = endPoint.getM();
            yi = endPoint.getB();
        }


        double m = (yj - yi) / (xj - xi);
        double b = ((xj * yi) - (xi * yj)) / (xj - xi);
        return new Point(m, b);
    }

    /**
     * @return Steigung
     */
    public double getSlope() {
        return medianX * -1;
    }

    /**
     * @return y-Achsenabschnitt
     */
    public double getYInterception() {
        return medianY * -1;
    }

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