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

import de.wwwu.awolf.model.Interval;
import de.wwwu.awolf.model.Line;
import de.wwwu.awolf.model.Point;
import de.wwwu.awolf.model.communication.AlgorithmData;
import de.wwwu.awolf.model.communication.Data;
import de.wwwu.awolf.model.communication.SubscriberType;
import de.wwwu.awolf.presenter.AbstractPresenter;
import de.wwwu.awolf.presenter.algorithms.Algorithm;
import de.wwwu.awolf.presenter.util.BinomialCoeffizient;
import de.wwwu.awolf.presenter.util.FastElementSelector;
import de.wwwu.awolf.presenter.util.IntersectionComputer;
import de.wwwu.awolf.presenter.util.RandomSampler;

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


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

    private final double POSITIV_INF = 9999.0;
    private final double NEGATIV_INF = -9999.0;
    private final double EPSILON = 0.00001;
    private List<Line> setOfLines;
    //Hilfsvariablen (siehe original Paper)
    private double j;
    private int jA;
    private int jB;
    private double r;
    private int n;
    private double N;
    private int k;
    //Intervall und die temporaeren Grenzen
    private Interval interval;
    private double aVariant;
    private double bVariant;
    private double slope;
    private double yInterception;
    private Flow.Subscriber<? super AlgorithmData> subscriber;
    private AbstractPresenter presenter;


    /**
     * Randomisierter Algorithmus zur Berechnung des Theil-Sen Schätzers.
     * Algorithmus stammt aus dem Paper:
     * "Jiri Matousek, Randomized optimal algorithm for slope selection,
     * Information Processing Letters 39 (1991) 183-187
     */
    public Line call() {

        this.n = this.setOfLines.size();
        this.N = BinomialCoeffizient.run(n, 2);
        //this.k = Integer.valueOf((int) (N * 0.5)) - 1;
        this.k = (int) (N / 2);

        interval = new Interval(NEGATIV_INF, POSITIV_INF);
        //damit eine initiale Ordnung herscht
        //Collections.sort(intervalIntersections);

        r = n;
        List<Point> intervalIntersections = new LinkedList<>(IntersectionComputer.getInstance().compute(setOfLines, interval.getLower(), interval.getUpper()));
        while (true) {
            if (this.N <= n || (Math.abs(interval.getUpper() - interval.getLower())) < EPSILON) {
                break;
            } else {
                //Anzahl der Schnittpunkte im Intervall [-Inf, a)
                int numberOfIntersections = getIntervalSize(NEGATIV_INF, interval.getLower());

                //Randomized Interpolating Search
                j = (r / N) * (double) (k - numberOfIntersections);
                jA = (int) Math.max(1, Math.floor(j - (1.5 * Math.sqrt(r))));
                jB = (int) Math.min(r, Math.floor(j + (1.5 * Math.sqrt(r))));


                /* Suche nach einem passenderen und kleineren Intervall
                   Schleife terminiert wenn die das k-te Elemnet zwischen aVariant und bVariant liegt und
                   das Intrvall weniger als 11*N / sqrt(r) Elemente besitzt */
                do {
                    //zufällige Stichprobe
                    List<Point> sampledIntersections = RandomSampler.run(intervalIntersections, r);

                    aVariant = FastElementSelector.randomizedSelect(getIntersectionAbscissas(sampledIntersections), jA);
                    bVariant = FastElementSelector.randomizedSelect(getIntersectionAbscissas(sampledIntersections), jB);
                } while (!checkCondition());

                interval.setLower(aVariant);
                interval.setUpper(bVariant);
                intervalIntersections = getOpenIntervalElements(interval.getLower(), interval.getUpper());
                N = getIntervalSize(interval.getLower(), interval.getUpper());
            }
        }

        return pepareResult();
    }

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

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

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

    private List<Double> getIntersectionAbscissas(List<Point> interections) {
        List<Double> abscissas = new ArrayList<>();
        interections.forEach(e -> abscissas.add(e.getX()));
        return abscissas;
    }


    /**
     * Diese Funktion überprüft ob die Bedingung für das Interval erfüllt ist. Dabei muss der k-te
     * Schnittpunkt in diesem Interval enthalten sein. des weiteren soll die Anzahl der Schnittpunkte
     * im Interval kleiner oder gleich dem Term: (11*N)/sqrt(r) sein.
     *
     * @return Boolscher Wert ob die Bedingung erfüllt ist
     */
    private Boolean checkCondition() {
        //Double kthElement = FastElementSelector.randomizedSelect(xCoordinates, k);
        //Boolean cond1 = (kthElement > aVariant) && (kthElement <= bVariant);

        int lowerCount = getIntervalSize(NEGATIV_INF, aVariant);
        int higherCount = getIntervalSize(NEGATIV_INF, bVariant);

        Boolean conda = k > lowerCount;
        Boolean condb = k <= higherCount;

        Boolean cond1 = conda && condb;

        Boolean cond2 = (higherCount - lowerCount) <= ((11 * N) / Math.sqrt(r));

        return (cond1 && cond2) || (aVariant == bVariant);
    }


    /**
     * Berechne wieviele von den Schnittpunkten in dem Interval zwischen <code>a</code> und <code>b</code>
     * enthalten sind.
     *
     * @param a untere Grenze des Intervals
     * @param b obere Grenze des Intrvals
     * @return Anzahl der Schnittpunkte im Interval [a,b)
     */
    public int getIntervalSize(double a, double b) {
        return getOpenIntervalElements(a, b).size();
    }

    /**
     * Berechne wieviele von den Schnittpunkten in dem Interval zwischen <code>a</code> und <code>b</code>
     * enthalten sind. Zusätzlich werden diese Schnittpunkte in einer Liste festgehalten und diese werden
     * zurückgeliefert.
     *
     * @param a untere Grenze des Intervals
     * @param b obere Grenze des Intrvals
     * @return Liste der Schnittpunkte die im Interval (a,b) vertreten sind
     */
    public List<Point> getOpenIntervalElements(double a, double b) {
        Collection<Point> intersections = IntersectionComputer.getInstance().compute(setOfLines, a, b);
        return new ArrayList<>(intersections);
    }

    private Line pepareResult() {
        double m, x;
        double b, y;

        List<Point> resultSt = getOpenIntervalElements(interval.getLower(), interval.getUpper());
        List<Double> resultAbscissas = new ArrayList<>();

        for (Point p : resultSt) {
            resultAbscissas.add(p.getX());
        }

        List<Double> yCoords = new ArrayList<>();

        for (Point p : getOpenIntervalElements(interval.getLower(), interval.getUpper())) {
            yCoords.add(p.getY());
        }

        double pseudoIndex = getIntervalSize(NEGATIV_INF, interval.getLower()) * 1.0;
        m = FastElementSelector.randomizedSelect(resultAbscissas, k - pseudoIndex);

        Set<Double> unique = new LinkedHashSet<>(yCoords);
        yCoords.clear();
        yCoords.addAll(unique);
        b = FastElementSelector.randomizedSelect(yCoords, yCoords.size() * 0.5) * -1;
        slope = m;
        yInterception = b;

        if (this.subscriber != null) {
            AlgorithmData data = new AlgorithmData();
            data.setType(SubscriberType.TS);
            data.setLineData(new Line(m, b));
            this.subscriber.onNext(data);
        }

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

    /**
     * @return Steigung
     */
    public Double getSlope() {
        return slope;
    }

    /**
     * @return y-Achsenabschnitt
     */
    public Double getYInterception() {
        return yInterception;
    }

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