package presenter.algorithms.util;

import model.*;
import presenter.algorithms.Comparators.YOrderLineComparatorBegin;
import presenter.algorithms.Comparators.YOrderLineComparatorEnd;

import java.util.*;

/**
 * Implementierung verschiedener Algorithmen zur Berechnung von Ausgleichsgeraden.
 *
 * @Author: Armin Wolf
 * @Email: a_wolf28@uni-muenster.de
 * @Date: 18.06.2017.
 */
public class IntersectionCounter {

    private HashMap<Integer, Integer> dictionaryTO;
    private HashMap<Integer, Integer> dictionaryBACK;
    private ArrayList<Integer> substituted;
    private ArrayList<Pair> inversions;
    private List<Line> set;

    //indexieren der Punkte damit die schnittpunkte berechnet werden können
    private HashMap<Line, Integer> secondaryDictionaryTO;
    private HashMap<Integer, Line> secondaryDictionaryBACK;
    private ArrayList<Line> umin;


    /**
     * Berechnet die Inversionen zwischen zwei Listen mit Integer Werten. Diese Methode dient als
     * Wrapper Methode. Die Logik steht in der <code>countInversions</code> Funktion.
     *
     * @param a Liste
     * @param b Liste
     * @return Anzahl an Inversionen
     */
    public int run(List<Integer> a, List<Integer> b) {

        dictionaryTO = new HashMap<>();
        dictionaryBACK = new HashMap<>();
        substituted = new ArrayList<>();
        inversions = new ArrayList<>();

        ArrayList<Integer> temp = new ArrayList<>();

        temp.addAll(a);

        for (int i = 0; i < a.size(); i++) {
            dictionaryTO.put(a.get(i), i + 1);
            dictionaryBACK.put(i + 1, a.get(i));
        }

        for (int j = 0; j < b.size(); j++) {
            substituted.add(dictionaryTO.get(b.get(j)));
        }

        int ret = countInversions(substituted, 0, substituted.size() - 1, temp);

        return ret;
    }

    /**
     * Wrapper Methode um herauszufinden wieviele Inversionen zwischen den Schnittpunkten der Werte
     * in der Liste und den Endpunkten des Intervalls entstehen.
     *
     * @param set      Liste mit Werten (m,b) um Schnittpunkte zu berechnen
     * @param interval Interval
     * @return Anzahl an Inversionen
     */
    public int run(List<Line> set, Interval interval) {
        ArrayList<Integer> listA = new ArrayList<>();
        ArrayList<Integer> listB = new ArrayList<>();

        prepareData(set, interval, listA, listB);
        return run(listA, listB);
    }

    /**
     * Methode die, die Daten für die Funktion <code>run</code> vorbereitet. Es werden die Schnittpunkte
     * bzgl. der unteren und oberen Grenze des Intervals und den Werten der Liste (m,b) berechnet. Diese
     * Werte haben die selbe x Koordinate aber verschiedene y Koordinaten.
     *
     * @param set      Liste mit Werten m,b
     * @param interval Interval
     * @param listA    Schnittpunkte bzgl. unteren Grenze
     * @param listB    Schnittpunkte bzgl. oberen Grenze
     */
    private void prepareData(List<Line> set, Interval interval, ArrayList<Integer> listA,
                             ArrayList<Integer> listB) {
        secondaryDictionaryTO = new HashMap<>();
        secondaryDictionaryBACK = new HashMap<>();
        this.set = set;
        umin = new ArrayList<>();
        Line tmpLine;

        for (Line p : set) {
            //vertauscht das Point standardmäßig die x lexikografische Ordnung betrachtet
            tmpLine = new Line(p.getM(), p.getM(), interval.getLower() * p.getM() + p.getB(),
                    interval.getUpper() * p.getM() + p.getB());
            //wird benötigt um späer die Schnittpunkte ermitteln zu können
            tmpLine.setB(p.getB());
            tmpLine.setM(p.getM());
            umin.add(tmpLine);
        }

        for (int i = 0; i < umin.size(); i++) {
            secondaryDictionaryTO.put(umin.get(i), i);
            secondaryDictionaryBACK.put(i, this.set.get(i));
        }

        Collections.sort(umin, new YOrderLineComparatorBegin());
        for (Line q : umin) {
            listA.add(secondaryDictionaryTO.get(q));
        }

        Collections.sort(umin, new YOrderLineComparatorEnd());
        for (Line q : umin) {
            listB.add(secondaryDictionaryTO.get(q));
        }

    }


    /**
     * Angepasster Merge-Sort Algorithmus.
     * Die Funktion bekommt neben den standard Parametern zusätzlich eine Liste mit Elementen
     * die als Groundtruth dienen.
     *
     * @param a     Eingabefeld mit den Elementen die überprüft werden sollen.
     * @param start Startpunkt des Eingabefeldes.
     * @param end   Endpunkt des Eingabefeldes.
     * @param aux   Temporäres Array das beim Mergen eine Kopie des original Arrays ist.
     * @return Anzahl der inversionen zwischen a und aux.
     */
    public int countInversions(List<Integer> a, int start, int end, List<Integer> aux) {
        if (start >= end) {
            return 0;
        }
        int invCount = 0;
        int mid = start + (end - start) / 2;
        int invCountLeft = countInversions(a, start, mid, aux); // divide and conquer
        int invCountRight = countInversions(a, mid + 1, end, aux); // divide and conquer
        invCount += (invCountLeft + invCountRight);
        for (int i = start; i <= end; i++) {
            aux.set(i, a.get(i));
        }
        int left = start;
        int right = mid + 1;
        int index = start;
        //hier beginnt das merging
        //iteriere über die Teillisten
        //Two-way Merge - Knuth Vol 3 The Art of Computer Programming -
        while (left <= mid && right <= end) {
            //wenn die linke Teilliste das kleinere Element besitzt kopiere
            //das Element in das neue Array
            if (aux.get(left) < aux.get(right)) {
                a.set(index++, aux.get(left++));
            } else {
                //merke die inversionspaare
                for (int i = left; i <= mid; i++) {
                    // System.out.println(aux.get(i)+" -- "+ aux.get(right));
                    inversions.add(new Pair(aux.get(i), aux.get(right)));
                }
                a.set(index++, aux.get(right++));
                invCount += mid - left + 1; // number of inversions for aux[right]
            }
        }
        while (left <= mid) {
            a.set(index++, aux.get(left++));
        }
        // no need to copy over remaining aux[right++] because they are already inside a
        return invCount;
    }

    /**
     * Diese Methode liefert nur nach dem Ausführen der <code>run</code> Funktion Sinnvolle Werte.
     *
     * @return Map mit Schnittpunkt Paaren.
     */
    public HashMap<Line, ArrayList<Line>> getIntersectionLinePairs() {
        ArrayList<Pair> result = new ArrayList<>();
        HashMap<Line, ArrayList<Line>> ret = new HashMap<>();

        for (int i = 0; i < inversions.size(); i++) {
            result.add(new Pair(dictionaryBACK.get(inversions.get(i).getP1()),
                    dictionaryBACK.get(inversions.get(i).getP2())));
        }
        ArrayList<Line> linePairs;

        for (Pair p : result) {
            Line l1 = secondaryDictionaryBACK.get(p.getP1());
            Line l2 = secondaryDictionaryBACK.get(p.getP2());
            if (ret.get(l2) == null) {
                linePairs = new ArrayList<>();
            } else {
                linePairs = ret.get(l2);
            }
            linePairs.add(l1);
            ret.put(l2, linePairs);

            //Symetrie
            if (ret.get(l1) == null) {
                linePairs = new ArrayList<>();
            } else {
                linePairs = ret.get(l1);
            }
            linePairs.add(l2);
            ret.put(l1, linePairs);
        }
    /*System.out.println("----------------------------------------------------------");
    for (Line outerLine : ret.keySet()){
      System.out.println("Linie: "+outerLine);
      for (Line innerLine : ret.get(outerLine)){
        System.out.println("\t\t -> "+innerLine);
      }
    }
    System.out.println("----------------------------------------------------------");*/

        return ret;
    }

    /**
     * Diese Methode liefert nur nach dem Ausführen der <code>run</code> Funktion Sinnvolle Werte.
     */
    public void calculateIntersectionAbscissas(LineModel model) {
        ArrayList<Pair> result = new ArrayList<>();

        for (int i = 0; i < inversions.size(); i++) {
            result.add(new Pair(dictionaryBACK.get(inversions.get(i).getP1()),
                    dictionaryBACK.get(inversions.get(i).getP2())));
        }

        for (Pair p : result) {
            Line line = secondaryDictionaryBACK.get(p.getP1());
            Line sampledLine = secondaryDictionaryBACK.get(p.getP2());
            if (!line.equals(sampledLine)) {
                double intersection = (line.getB() - sampledLine.getB()) / (sampledLine.getM() - line.getM());
                double yintercept = sampledLine.getM() * intersection + sampledLine.getB();
                model.addNode(new Point(intersection, yintercept));
            }
        }

    }


    /**
     * Diese Methode liefert nur nach dem Ausführen der <code>run</code> Funktion Sinnvolle Werte.
     */
    public ArrayList<Point> calculateIntersectionAbscissas() {
        ArrayList<Pair> result = new ArrayList<>();

        ArrayList<Point> points = new ArrayList<>();
        for (int i = 0; i < inversions.size(); i++) {
            result.add(new Pair(dictionaryBACK.get(inversions.get(i).getP1()),
                    dictionaryBACK.get(inversions.get(i).getP2())));
        }

        for (Pair p : result) {
            Line line = secondaryDictionaryBACK.get(p.getP1());
            Line sampledLine = secondaryDictionaryBACK.get(p.getP2());
            if (!line.equals(sampledLine)) {
                double intersection = (line.getB() - sampledLine.getB()) / (sampledLine.getM() - line.getM());
                double yintercept = sampledLine.getM() * intersection + sampledLine.getB();
                Point point = new Point(intersection, yintercept);
                points.add(point);
            }
        }

        return points;

    }

    /**
     * Berechnet die Schnittpunkte zwischen einer gegebenen Gerade und einer Menge an Geraden.
     *
     * @param set         Menge an Geraden
     * @param sampledLine eine spezielle Gerade
     * @return Liste mit x Koordinaten der Schnittpunkte
     */
    public ArrayList<Double> calculateIntersectionAbscissas(ArrayList<Line> set, Line sampledLine) {
        LinkedList<Line> lines = new LinkedList<>(set);
        ArrayList<Double> intersections = new ArrayList<>();
        double intersection;

        for (Line line : lines) {
            if (line != sampledLine) {
                intersection = (line.getB() - sampledLine.getB()) / (sampledLine.getM() - line.getM());
                intersections.add(intersection);
            }
        }

        return intersections;
    }


}
