package presenter.algorithms.advanced;

import model.Interval;
import model.Line;
import model.Point;
import presenter.Presenter;
import presenter.algorithms.Algorithm;
import presenter.algorithms.util.BinomialCoeffizient;
import presenter.algorithms.util.FastElementSelector;
import presenter.algorithms.util.RandomSampler;

import java.util.*;

/**
 * Implementierung verschiedener Algorithmen zur Berechnung von Ausgleichsgeraden.
 *
 * @Author: Armin Wolf
 * @Email: a_wolf28@uni-muenster.de
 * @Date: 28.05.2017.
 */
public class TheilSenEstimator extends Observable implements Algorithm {

    private final Double POSITIV_INF = 99999.0;
    private final Double NEGATIV_INF = -99999.0;
    private final Double EPSILON = 0.00001;
    private Presenter presenter;
    private ArrayList<Line> setOfLines;
    private ArrayList<Point> setOfIntersections;
    private ArrayList<Point> intervalIntersections;
    private ArrayList<Double> sampledIntersections;
    //wird benötigt um den y Achsenabschnitt zu Berechnen
    private ArrayList<Double> yCoordinates;
    private ArrayList<Double> xCoordinates;
    //Hilfsvariablen (siehe original Paper)
    private Double j;
    private Integer jA;
    private Integer jB;
    private Double r;
    private Integer n;
    private Double N;
    private Integer k;
    //Intervall und die temporaeren Grenzen
    private Interval interval;
    private Double aVariant;
    private Double bVariant;
    private Double slope;
    private Double yInterception;


    public TheilSenEstimator(LinkedList<Line> setOfLines, ArrayList<Point> setOfIntersections, Presenter presenter) {
        this.presenter = presenter;
        this.setOfLines = new ArrayList<>(setOfLines);
        this.setOfIntersections = new ArrayList<>(setOfIntersections);
        this.intervalIntersections = new ArrayList<>(setOfIntersections);

        this.n = setOfLines.size();
        this.sampledIntersections = new ArrayList<>();
        this.yCoordinates = new ArrayList<>();
        this.xCoordinates = new ArrayList<>();
        this.N = BinomialCoeffizient.run(n, 2);
        this.k = Integer.valueOf((int) (N * 0.5)) - 1;
    }

    public TheilSenEstimator(LinkedList<Line> setOfLines, ArrayList<Point> setOfIntersections) {
        this(setOfLines, setOfIntersections, null);
    }

    /**
     * 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 void run() {
        //damit eine initiale Ordnung herscht
        //Collections.sort(intervalIntersections);

        interval = new Interval(NEGATIV_INF, POSITIV_INF);
        r = (double) n;
        while (true) {
            if (this.N <= n || (Math.abs(interval.getUpper() - interval.getLower())) < EPSILON) {
                break;
            } else {
                //Anzahl der Schnittpunkte im Intervall [-Inf, a)
                int numberOfIntersections = getOpenIntervalSize(NEGATIV_INF, interval.getLower(), setOfIntersections);

                //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
                    sampledIntersections = RandomSampler.run(intervalIntersections, r);
                    aVariant = FastElementSelector.randomizedSelect(sampledIntersections, jA);
                    bVariant = FastElementSelector.randomizedSelect(sampledIntersections, jB);
                } while (!checkCondition());

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


    /**
     * 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, setOfIntersections);
        int higherCount = getIntervalSize(NEGATIV_INF, bVariant, setOfIntersections);

        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, ArrayList<Point> set) {
        int counter = 0;
        for (Point x : set) {
            if (x.getX() >= a && x.getX() < b) {
                counter++;
            }
        }
        return counter;
    }

    /**
     * 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 getOpenIntervalSize(double a, double b, ArrayList<Point> set) {
        int counter = 0;
        for (int i = 0; i < set.size(); i++) {
            Point x = set.get(i);
            if (x.getX() > a && x.getX() < b) {
                counter++;
            }
        }
        return counter;
    }

    /**
     * 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 ArrayList<Point> getOpenIntervalElements(double a, double b) {
        ArrayList<Point> list = new ArrayList<>();
        for (int i = 0; i < intervalIntersections.size(); i++) {
            Point x = intervalIntersections.get(i);
            if ((x.getX() > a && x.getX() < b) || (Math.abs(interval.getUpper() - interval.getLower())) < EPSILON) {
                list.add(x);
            }
        }
        intervalIntersections.clear();
        intervalIntersections = null;
        return list;
    }

    @Override
    public void getResult() {
        double m, x;
        double b, y;

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

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

        for (Point p : setOfIntersections) {
            yCoordinates.add(p.getY());
        }

        int pseudoIndex = (int) getOpenIntervalSize(NEGATIV_INF, interval.getLower(), setOfIntersections);
        m = FastElementSelector.randomizedSelect(resultAbscissas, k - pseudoIndex);

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

        if (presenter != null) {
            setChanged();

            String[] result = new String[]{"ts", m + "", b + ""};
            notifyObservers(result);
        }
    }


    public Double getSlope() {
        return slope;
    }

    public Double getyInterception() {
        return yInterception;
    }
}
