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.FastElementSelector;
import de.wwwu.awolf.presenter.util.IntersectionComputer;
import de.wwwu.awolf.presenter.util.Logging;
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 RepeatedMedianEstimator implements Algorithm {


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

    private AbstractPresenter presenter;
    private List<Line> setOfLines;
    private Interval interval;
    private Interval original;

    //in der Literatur als L_i, C_i, und R_i bekannt
    private int countLeftSlab;
    private int countCenterSlab;
    private int countRightSlab;

    //in der Literatur als L_i, C_i, und R_i bekannt
    private Set<Point> intersectionsInLeftSlab;
    private Set<Point> intersectionsInCenterSlab;
    private Set<Point> intersectionsInRightSlab;

    private double r;
    private int n;
    private double k;
    private double kLow;
    private double kHigh;
    private double beta;

    private double slope;
    private double yInterception;
    private Flow.Subscriber<? super AlgorithmData> subscriber;


    /**
     * Führt den Algortihmus zur Berechnung des RM-Schätzers durch.
     * <p>
     * Paper:
     * Matousek, Jiri, D. M. Mount und N. S. Netanyahu
     * „Efficient Randomized Algorithms for the Repeated Median Line Estimator“. 1998
     * Algorithmica 20.2, S. 136–150
     */
    public Line call() {

        n = setOfLines.size();
        interval = new Interval(-10000, 10000);
        original = new Interval(-10000, 10000);
        beta = 0.5;

        intersectionsInLeftSlab = new HashSet<>();
        intersectionsInCenterSlab = new HashSet<>();
        intersectionsInRightSlab = new HashSet<>();

        intersectionsInLeftSlab.add(new Point(0d, 0d));
        intersectionsInCenterSlab.add(new Point(0d, 0d));
        intersectionsInCenterSlab.add(new Point(0d, 0d));

        for (int i = 0; i < n; i++) {
            countLeftSlab = 0;
            countRightSlab = 0;
            countCenterSlab = setOfLines.size();
        }

        Logging.logInfo("=== S T A R T - R M ===");
        long start;
        long end;
        start = System.currentTimeMillis();
        double thetaLow = 0;
        double thetaHigh = 0;

        while (countCenterSlab > 1) {
            n = countCenterSlab;
            r = Math.ceil(Math.pow(n, beta));
            List<Line> lines = RandomSampler.run(setOfLines, r);

            //Für jede Gerade aus der Stichprobe wird der Schnittpunkt mit der medianen
            //x-Koordinate bestimmt
            List<Double> medianIntersectionAbscissas = new ArrayList<>();
            final double lowerBound = thetaLow;
            final double upperBound = thetaHigh;
            if (!lines.isEmpty()) {
                lines.forEach(line -> medianIntersectionAbscissas.add(estimateMedianIntersectionAbscissas(lines, line)));
            }

            //Rang vom RM-Wert in C
            k = Math.max(1, Math.min(n, (Math.ceil(n * 0.5) - countLeftSlab)));

            //berechne k_lo und k_hi
            computeSlabBorders();

            //Berechne die Elemente mit dem Rang Theta_lo und Theta_hi
            Collections.sort(medianIntersectionAbscissas);
            thetaLow = FastElementSelector.randomizedSelect(medianIntersectionAbscissas, kLow);
            thetaHigh = FastElementSelector.randomizedSelect(medianIntersectionAbscissas, kHigh);

            //Für jede Gerade in C wird die Anzahl der Schnittpunkte die im Intervall liegen hochgezählt
            countNumberOfIntersectionsAbscissas(thetaLow, thetaHigh);

            //verkleinere das Intervall
            contractIntervals(thetaLow, thetaHigh);

        }

        end = System.currentTimeMillis();
        Logging.logInfo("=== E N D - R M ===  " + ((end - start) / 1000));

        return pepareResult(thetaLow, thetaHigh);
    }

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

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

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


    /**
     * Berechnet die mediane x-Koordinate über den Schnittpunkten.
     *
     * @param sampledLine Stichprobe von Geraden
     * @return mediane x-Koordinate über den Schnittpunkten
     */
    public Double estimateMedianIntersectionAbscissas(List<Line> lines, Line sampledLine) {

        List<Double> intersections = IntersectionComputer.getInstance().calculateIntersectionAbscissas(lines, sampledLine, original.getLower(), original.getUpper());
        List<Double> left = IntersectionComputer.getInstance().calculateIntersectionAbscissas(lines, sampledLine, original.getLower(), interval.getLower());
        List<Double> center = IntersectionComputer.getInstance().calculateIntersectionAbscissas(lines, sampledLine, interval.getLower(), interval.getUpper());


        double ki = Math.ceil((n - 1) * 0.5) - left.size();
        double i = (Math.ceil((Math.sqrt(n) * ki) / center.size()));
        int accessIndex;
        if (i < 0)
            accessIndex = 0;
        else if (i >= intersections.size() && !intersections.isEmpty())
            accessIndex = intersections.size() - 1;
        else
            accessIndex = (int) i;

        return FastElementSelector.randomizedSelect(intersections, accessIndex);
    }

    /**
     * Berechnet die potenziell neuen Intervallgrenzen.
     */
    public void computeSlabBorders() {
        kLow = Math.max(1, Math.floor(((r * k) / (countCenterSlab))
                - ((3 * Math.sqrt(r)) * (0.5))));
        kHigh = Math.min(r, Math.floor(((r * k) / (countCenterSlab))
                + ((3 * Math.sqrt(r)) * (0.5))));
    }


    /**
     * Berechnet die Anzahl der Schnittpunkte pro Bereich. Insgesammt gibt es drei Bereiche:
     * Im Intervall => (a,b], vor dem Intervall => (a', a], hinter dem Intervall => (b, b'].
     */
    public void countNumberOfIntersectionsAbscissas(final double lower, final double upper) {
        IntersectionComputer instance = IntersectionComputer.getInstance();


        intersectionsInLeftSlab = new HashSet<>(instance.compute(setOfLines, interval.getLower(), lower));
        intersectionsInCenterSlab = new HashSet<>(instance.compute(setOfLines, lower, upper));
        intersectionsInRightSlab = new HashSet<>(instance.compute(setOfLines, upper, interval.getUpper()));

        int tmp = new HashSet<>(instance.compute(setOfLines, interval.getLower(), interval.getUpper())).size();
        countLeftSlab = intersectionsInLeftSlab.size();
        countCenterSlab = intersectionsInCenterSlab.size();
        countRightSlab = intersectionsInRightSlab.size();
    }

    /**
     * Verkleinert das aktuelle Intervall. Eines der drei Bereiche wird als neues Intervall gewählt.
     * Auf diesem Intervall werden dann in der nächsten Iteration wieder drei Bereiche bestimmt.
     */
    public void contractIntervals(final double lower, final double upper) {
        double max = Math.max(countLeftSlab, Math.max(countCenterSlab, countRightSlab));

        boolean newIntervalIsC = countLeftSlab < Math.ceil(n * 0.5) && Math.ceil(n * 0.5) <= countLeftSlab + countCenterSlab;
        boolean newIntervalIsL = Math.ceil(n * 0.5) <= countLeftSlab;
        boolean newIntervalIsR = countLeftSlab + countCenterSlab < Math.ceil(n * 0.5) && Math.ceil(n * 0.5) <= (countLeftSlab + countCenterSlab + countRightSlab);

        //wähle C als C
        if (newIntervalIsC) {
            interval.setLower(lower);
            interval.setUpper(upper);
        } else if (newIntervalIsL) {
            interval.setUpper(lower);
        } else if (newIntervalIsR) {
            interval.setLower(upper);
        }
    }

    private Line pepareResult(final double thetaLow, final double thetaHigh) {
        slope = thetaLow;
        List<Double> potentialYInterceptions = new ArrayList<>();
        setOfLines.forEach(line -> {
            potentialYInterceptions.add(line.getB() - (slope * line.getM()));
        });

        yInterception = FastElementSelector.randomizedSelect(potentialYInterceptions, Math.floor(potentialYInterceptions.size() * 0.5));

        if (this.subscriber != null) {
            AlgorithmData data = new AlgorithmData();
            data.setType(SubscriberType.RM);
            data.setLineData(new Line(getSlope(), getyInterception()));
            this.subscriber.onNext(data);
        }

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

    /**
     * @return Anzahl der Geraden
     */
    public Integer getN() {
        return n;
    }

    /**
     * @param n Anzahl der Geraden
     */
    public void setN(Integer n) {
        this.n = n;
    }

    /**
     * @param beta Parameter Beta
     */
    public void setBeta(Double beta) {
        this.beta = beta;
    }

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

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

    /**
     * @return temporäres untere Intervallgrenze
     */
    public Double getkLow() {
        return kLow;
    }

    /**
     * @param kLow temporäres untere Intervallgrenze
     */
    public void setkLow(Double kLow) {
        this.kLow = kLow;
    }

    /**
     * @return temporäres oberes Intervallgrenze
     */
    public Double getkHigh() {
        return kHigh;
    }

    /**
     * @param kHigh temporäres oberes Intervallgrenze
     */
    public void setkHigh(Double kHigh) {
        this.kHigh = kHigh;
    }

    /**
     * @return verteilung der Punkte
     */
    public int getCountLeftSlab() {
        return countLeftSlab;
    }

    /**
     * @return verteilung der Punkte
     */
    public int getCountCenterSlab() {
        return countCenterSlab;
    }

    /**
     * @return verteilung der Punkte
     */
    public int getCountRightSlab() {
        return countRightSlab;
    }

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


