package presenter.algorithms.advanced;

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

import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Observable;
import java.util.concurrent.ThreadLocalRandom;


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

    private Presenter presenter;
    private LinkedList<Line> set;
    private HashMap<Line, ArrayList<Line>> linePairs;
    private HashMap<Line, Double> medianIntersections = new HashMap<>();
    private HashMap<Line, ArrayList<Double>> intersectionAbscissas = new HashMap<>();
    private Interval interval;

    //in der Literatur als L_i, C_i, und R_i bekannt
    private ArrayList<Double> countLeftSlab;
    private ArrayList<Double> countCenterSlab;
    private ArrayList<Double> countRightSlab;

    //die Mengen L,C und R
    private ArrayList<Line> linesInLeftSlab;
    private ArrayList<Line> linesInCenterSlab;
    private ArrayList<Line> linesInRightSlab;

    private Double r;
    private Integer n;
    private Double k;
    private Double kLow;
    private Double kHigh;
    private Double beta;

    private Double thetaLow;
    private Double thetaHigh;

    private Double slope;
    private Double yInterception;


    public RepeatedMedianEstimator(LinkedList<Line> set, Presenter presenter) {
        this.set = set;
        this.presenter = presenter;
        interval = new Interval(-10000, 10000);
        n = set.size();
        beta = 0.5;
        countLeftSlab = new ArrayList<>();
        countCenterSlab = new ArrayList<>();
        countRightSlab = new ArrayList<>();

        for (int i = 0; i < n; i++) {
            countLeftSlab.add(0d);
            countRightSlab.add(0d);
            countCenterSlab.add(n - 1.0);
            intersectionAbscissas.put(set.get(i), new ArrayList<>());
        }

        linesInLeftSlab = new ArrayList<>();
        linesInCenterSlab = new ArrayList<>(set);
        linesInRightSlab = new ArrayList<>();
        linePairs = new HashMap<>();
    }

    public RepeatedMedianEstimator(LinkedList<Line> set) {
        this(set, null);
    }

    /**
     *
     */
    public void run() {

        while (linesInCenterSlab.size() != 1) {
            n = linesInCenterSlab.size();
            r = Math.ceil(Math.pow(n, beta));
            ArrayList<Line> lines = RandomSampler.run(linesInCenterSlab, r, linesInCenterSlab.size());


            //For each Sampled Line, compute its median intersection abscissa
            ArrayList<Double> medianIntersectionAbscissas = new ArrayList<>();
            for (Line l : lines) {
                Double abscissa = estimateMedianIntersectionAbscissas(l);
                medianIntersections.put(l, abscissa);
                medianIntersectionAbscissas.add(abscissa);
            }

            //rank of the repeated median in C
            k = Math.max(1, Math.min(set.size(), (Math.ceil(n * 0.5) - linesInLeftSlab.size())));

            //compute k_lo and k_hi
            computeSlabBorders();


            //  if (medianIntersectionAbscissas.size() < kLow || medianIntersectionAbscissas.size()<kHigh || kLow < 0 || kHigh<0 )
            //      System.err.print("#medianIntersectionAbscissa: "+medianIntersectionAbscissas.size()+"\t\t klow: "+kLow+" kHigh: "+kHigh+"\n\n");

            //Employ fast selection algorithm to determine the Elements Theta_lo and Theta_hi
            thetaLow = FastElementSelector.randomizedSelect(medianIntersectionAbscissas, kLow);
            thetaHigh = FastElementSelector.randomizedSelect(medianIntersectionAbscissas, kHigh);

            //For each dual Line in C count the number of intersection abscissas that lie
            //in each of the intervals.
            countNumberOfIntersectionsAbscissas();

            //Based on this 3 counts, determine which of the subintervals contains the repeated median
            //and contract to this subiinterval.
            contractIntervals();

        }
    }

    /**
     * @param set
     * @param r
     * @return
     */
    public ArrayList<Line> sampleLines(ArrayList<Line> set, Double r) {

        ArrayList<Line> sampledLines = new ArrayList<>();

        for (int i = 0; i < r; i++) {
            sampledLines.add(set.get(ThreadLocalRandom.current().nextInt(0, linesInCenterSlab.size())));
        }

        return sampledLines;
    }

    /**
     * @param sampledLine
     * @return
     */
    public Double estimateMedianIntersectionAbscissas(Line sampledLine) {

        Integer index = Integer.parseInt(sampledLine.getId());
        ArrayList<Double> intersections = new ArrayList<>();
        double intersection;

        IntersectionCounter intersectionCounter = new IntersectionCounter();
        intersections = intersectionCounter.calculateIntersectionAbscissas(linesInCenterSlab, sampledLine);

        //Collections.sort(intersections);
        //double ki = Math.ceil((n - 1) / 2) - countLeftSlab.get(index);
        //double i = (Math.ceil((Math.sqrt(n) * ki) / countCenterSlab.get(index)));
        double ki = Math.ceil((n - 1) / 2) - FastElementSelector.randomizedSelect(countLeftSlab, index);
        double i = (Math.ceil((Math.sqrt(n) * ki) / FastElementSelector.randomizedSelect(countCenterSlab, index)));
        int accessIndex;
        if (i < 0)
            accessIndex = 0;
        else if (i >= intersections.size())
            accessIndex = intersections.size() - 1;
        else
            accessIndex = (int) i;

        //System.out.println(accessIndex);

        //return intersections.get(accessIndex);
        return FastElementSelector.randomizedSelect(intersections, accessIndex);
    }

    /**
     *
     */
    public void computeSlabBorders() {
        kLow = Math
                .max(1, Math.floor(
                        ((r * k) / (linesInCenterSlab.size()))
                                - ((3 * Math.sqrt(r)) / (2))
                        )
                );
        kHigh = Math
                .min(r, Math.floor(
                        ((r * k) / (linesInCenterSlab.size()))
                                + ((3 * Math.sqrt(r)) / (2))
                        )
                );
    }


    /**
     *
     */
    public void countNumberOfIntersectionsAbscissas() {

        for (Line line : linesInCenterSlab) {
            ArrayList<Double> intersections = intersectionAbscissas.get(line);
            Integer index = Integer.parseInt(line.getId());
            int left = 0;
            int center = 0;
            int right = 0;

            for (Double intersection : intersections) {
                if (intersection <= thetaLow) {
                    left++;
                } else if (intersection > thetaLow && intersection <= thetaHigh) {
                    center++;
                } else if (intersection > thetaHigh) {
                    right++;
                }
            }

            //System.out.println("Linie: "+line.getId()+"\tLeft: "+left+"\t Center: "+center+"\t Right: "+right);
            countLeftSlab.set(index, (double) left);
            countCenterSlab.set(index, (double) center);
            countRightSlab.set(index, (double) right);
        }

    }

    /**
     *
     */
    public void contractIntervals() {
        for (int i = 0; i < linesInCenterSlab.size(); i++) {

            double left = countLeftSlab.get(i);
            double center = countCenterSlab.get(i);
            double right = countRightSlab.get(i);

            double max = Math.max(left, Math.max(center, right));

            if (left == max) {
                linesInLeftSlab.add(linesInCenterSlab.get(i));
                linesInCenterSlab.remove(i);
            } else if (right == max) {
                linesInRightSlab.add(linesInCenterSlab.get(i));
                linesInCenterSlab.remove(i);
            }
            //      if (medianIntersections.get(linesInCenterSlab.get(i)) != null) {
            //        if (medianIntersections.get(linesInCenterSlab.get(i)) <= thetaLow) {
            //          linesInLeftSlab.add(linesInCenterSlab.get(i));
            //          linesInCenterSlab.remove(i);
            //          countLeftSlab.set(i, countLeftSlab.get(i) + 1);
            //          countCenterSlab.set(i, countCenterSlab.get(i) - 1);
            //        } else if (medianIntersections.get(linesInCenterSlab.get(i)) > thetaHigh) {
            //          linesInRightSlab.add(linesInCenterSlab.get(i));
            //          linesInCenterSlab.remove(i);
            //          countRightSlab.set(i, countRightSlab.get(i) + 1);
            //          countCenterSlab.set(i, countCenterSlab.get(i) - 1);
            //        }
            //      }
            //    }
            //
        }

        //wähle C als C
        if (linesInLeftSlab.size() < Math.ceil(n / 2) && Math.ceil(n / 2) <= linesInLeftSlab.size() + linesInCenterSlab.size()) {
            interval.setLower(thetaLow + 0.1);
            interval.setUpper(thetaHigh);
        }
        // wähle L als C
        else if (Math.ceil(n / 2) <= linesInLeftSlab.size()) {
            interval.setUpper(thetaLow);
        }
        //wähle R als C
        else if (linesInLeftSlab.size() + linesInCenterSlab.size() < Math.ceil(n / 2) && Math.ceil(n / 2) <= (linesInLeftSlab.size() + linesInCenterSlab.size() + linesInRightSlab.size())) {
            interval.setLower(thetaHigh - 0.1);
        }
    }

    @Override
    public void getResult() {
        if (presenter != null) {
            setChanged();
            double m = thetaLow;
            double b = (-1) * (
                    (linesInCenterSlab.get(0).getM() * (thetaLow)) + linesInCenterSlab.get(0)
                            .getB());

            slope = m;
            yInterception = b;
            String[] result = new String[]{"rm", m + "", b + ""};
            notifyObservers(result);
        } else {
            double m = thetaLow;
            double b = (-1) * ((linesInCenterSlab.get(0).getM() * (thetaLow)) + linesInCenterSlab.get(0).getB());
            slope = m;
            yInterception = b;
        }
    }

    /*******************************************************************************************************************
     *                                          Getter und Setter Methoden
     ******************************************************************************************************************/
    public LinkedList<Line> getSet() {
        return set;
    }

    public void setSet(LinkedList<Line> set) {
        this.set = set;
    }

    public HashMap<Line, ArrayList<Line>> getLinePairs() {
        return linePairs;
    }

    public void setLinePairs(HashMap<Line, ArrayList<Line>> linePairs) {
        this.linePairs = linePairs;
    }

    public HashMap<Line, Double> getMedianIntersections() {
        return medianIntersections;
    }

    public void setMedianIntersections(HashMap<Line, Double> medianIntersections) {
        this.medianIntersections = medianIntersections;
    }

    public HashMap<Line, ArrayList<Double>> getIntersectionAbscissas() {
        return intersectionAbscissas;
    }

    public void setIntersectionAbscissas(
            HashMap<Line, ArrayList<Double>> intersectionAbscissas) {
        this.intersectionAbscissas = intersectionAbscissas;
    }

    public Interval getInterval() {
        return interval;
    }

    public void setInterval(Interval interval) {
        this.interval = interval;
    }

    public ArrayList<Double> getCountLeftSlab() {
        return countLeftSlab;
    }

    public void setCountLeftSlab(ArrayList<Double> countLeftSlab) {
        this.countLeftSlab = countLeftSlab;
    }

    public ArrayList<Double> getCountCenterSlab() {
        return countCenterSlab;
    }

    public void setCountCenterSlab(ArrayList<Double> countCenterSlab) {
        this.countCenterSlab = countCenterSlab;
    }

    public ArrayList<Double> getCountRightSlab() {
        return countRightSlab;
    }

    public void setCountRightSlab(ArrayList<Double> countRightSlab) {
        this.countRightSlab = countRightSlab;
    }

    public ArrayList<Line> getLinesInLeftSlab() {
        return linesInLeftSlab;
    }

    public void setLinesInLeftSlab(ArrayList<Line> linesInLeftSlab) {
        this.linesInLeftSlab = linesInLeftSlab;
    }

    public ArrayList<Line> getLinesInCenterSlab() {
        return linesInCenterSlab;
    }

    public void setLinesInCenterSlab(ArrayList<Line> linesInCenterSlab) {
        this.linesInCenterSlab = linesInCenterSlab;
    }

    public ArrayList<Line> getLinesInRightSlab() {
        return linesInRightSlab;
    }

    public void setLinesInRightSlab(ArrayList<Line> linesInRightSlab) {
        this.linesInRightSlab = linesInRightSlab;
    }

    public Double getR() {
        return r;
    }

    public void setR(Double r) {
        this.r = r;
    }

    public Integer getN() {
        return n;
    }

    public void setN(Integer n) {
        this.n = n;
    }

    public Double getK() {
        return k;
    }

    public void setK(Double k) {
        this.k = k;
    }

    public Double getkLow() {
        return kLow;
    }

    public void setkLow(Double kLow) {
        this.kLow = kLow;
    }

    public Double getkHigh() {
        return kHigh;
    }

    public void setkHigh(Double kHigh) {
        this.kHigh = kHigh;
    }

    public Double getBeta() {
        return beta;
    }

    public void setBeta(Double beta) {
        this.beta = beta;
    }

    public Double getThetaLow() {
        return thetaLow;
    }

    public void setThetaLow(Double thetaLow) {
        this.thetaLow = thetaLow;
    }

    public Double getThetaHigh() {
        return thetaHigh;
    }

    public void setThetaHigh(Double thetaHigh) {
        this.thetaHigh = thetaHigh;
    }

    public Double getSlope() {
        return slope;
    }

    public Double getyInterception() {
        return yInterception;
    }
}


