package com.knecon.fforesight.service.layoutparser.processor.utils;

import java.awt.geom.Point2D;
import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.TreeMap;

import com.knecon.fforesight.service.layoutparser.processor.model.table.Ruling;

import lombok.experimental.UtilityClass;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@UtilityClass
public class RulingIntersectionFinder {

    public static final int PERPENDICULAR_UNIT_EXPAND_AMOUNT = 2;

    public static final Comparator<Point2D> Y_THEN_X_POINT_COMPARATOR = Comparator.comparingDouble(Point2D::getY).thenComparing(Point2D::getX);


    /**
     * Implementation to find line intersection in O(P + n log n), where n is the number of lines and P the numer of intersections
     * based on <a href="http://people.csail.mit.edu/indyk/6.838-old/handouts/lec2.pdf">Segment Intersection by Piotr Indyk</a>
     * The algorithm assumes there are only horizontal and vertical lines which are unique in their coordinates. (E.g. no overlapping horizontal lines exist)
     * As a high level overview, the algorithm uses a sweep line advancing from left to right.
     * It dynamically updates the horizontal rulings which are intersected by the current sweep line.
     * When the sweep line hits a vertical line, it then checks for all intersections with the currently intersected horizontal rulings.
     * THe trick of the algorithm is using a binary search tree to store the currently intersected horizontal rulings. This way the lookup should be in O(log n).
     * This way the initial sorting step has the highest complexity class (O(n log n) and thus determines the complexity class of the entire algorithm
     *
     * Unfortunately, the implementation here takes a few liberties compared to the original algorithm. The binary search tree is replaced by an ordered Set which is simply looped over.
     * Therefore, this implementation's worst case, where all horizontal lines span the entire sweep, you are essentially performing the naive approach with a bunch of overhead.
     * Since we are using this implementation to find table cells, one can expect this worst case to always be the case.
     *
     * A simple runtime comparison for a single page with the most lines we can expect (SinglePages/AbsolutelyEnormousTable.pdf with 30 horizontals and 144 verticals) shows this implementation takes roughly 14 ms, whereas the naive approach takes 7 ms. Both are negligible, but the naive approach is two times as fast.
     *
     * If we would like to make this faster, we would need a better data structure for 'TreeMap<Ruling, Void> horizontalRulingsInCurrentSweep', where we can query the TreeMap for all horizontal rulings in a given interval in O(log n).
     *
     * @param horizontals a list of non-overlapping horizontal rulings
     * @param verticals   a list of non-overlapping vertical rulings
     * @return a Map of each found intersection point pointing to the two lines forming the intersection.
     */
    public Map<Point2D, IntersectingRulings> find(List<Ruling> horizontals, List<Ruling> verticals) {

        long start = System.currentTimeMillis();
        List<SweepStep> sweepTrajectory = buildSweepTrajectory(horizontals, verticals);

        TreeMap<Ruling, Void> horizontalRulingsInCurrentSweep = new TreeMap<>(Comparator.comparingDouble(Ruling::getTop));

        TreeMap<Point2D, IntersectingRulings> intersections = new TreeMap<>(Y_THEN_X_POINT_COMPARATOR);

        for (SweepStep step : sweepTrajectory) {
            switch (step.type) {
                case VERTICAL: // check for intersections with currently intersected horizontal lines
                    for (Ruling horizontalRuling : horizontalRulingsInCurrentSweep.navigableKeySet()) {

                        Optional<Point2D> intersectionPoint = findIntersectionPoint(horizontalRuling, step.ruling);

                        if (intersectionPoint.isEmpty()) {
                            continue;
                        }

                        intersections.put(intersectionPoint.get(), new IntersectingRulings(horizontalRuling, step.ruling));
                    }
                    break;
                case HORIZONTAL_ENTRY: // sweep line now intersects this horizontal ruling
                    horizontalRulingsInCurrentSweep.put(step.ruling, null);
                    break;
                case HORIZONTAL_EXIT: // sweep line no longer intersects this horizontal ruling
                    horizontalRulingsInCurrentSweep.remove(step.ruling);
                    break;
            }
        }
        log.debug("Finished building intersections with line sweep in {} ms", System.currentTimeMillis() - start);

        return intersections;

    }


    /**
     * Naive Approach in O(n^2) of finding intersections between lines by iterating over all lines.
     *
     * @param horizontals a list of non-overlapping horizontal rulings
     * @param verticals   a list of non-overlapping vertical rulings
     * @return a Map of each found intersection point pointing to the two lines forming the intersection.
     */
    public Map<Point2D, IntersectingRulings> findNaive(List<Ruling> horizontals, List<Ruling> verticals) {

        long start = System.currentTimeMillis();
        TreeMap<Point2D, IntersectingRulings> intersections = new TreeMap<>(Y_THEN_X_POINT_COMPARATOR);

        for (Ruling horizontal : horizontals) {
            for (Ruling vertical : verticals) {
                Optional<Point2D> intersectionPoint = findIntersectionPoint(horizontal, vertical);

                if (intersectionPoint.isEmpty()) {
                    continue;
                }

                intersections.put(intersectionPoint.get(), new IntersectingRulings(horizontal, vertical));
            }
        }
        log.debug("Finished building intersections naively in {} ms", System.currentTimeMillis() - start);

        return intersections;
    }


    private static List<SweepStep> buildSweepTrajectory(List<Ruling> horizontals, List<Ruling> verticals) {

        List<SweepStep> sweepTrajectory = new LinkedList<>();

        for (Ruling horizontalRuling : horizontals) {
            sweepTrajectory.add(new SweepStep(SweepStep.Type.HORIZONTAL_ENTRY, horizontalRuling.getLeft() - PERPENDICULAR_UNIT_EXPAND_AMOUNT, horizontalRuling));
            sweepTrajectory.add(new SweepStep(SweepStep.Type.HORIZONTAL_EXIT, horizontalRuling.getRight() + PERPENDICULAR_UNIT_EXPAND_AMOUNT, horizontalRuling));
        }

        for (Ruling verticalRuling : verticals) {
            sweepTrajectory.add(new SweepStep(SweepStep.Type.VERTICAL, verticalRuling.getLeft(), verticalRuling));
        }

        Collections.sort(sweepTrajectory);

        return sweepTrajectory;
    }


    public Optional<Point2D> findIntersectionPoint(Ruling horizontal, Ruling vertical) {

        if (!horizontal.isHorizontal() || !vertical.isVertical()) {
            log.warn("lines must be orthogonal, vertical and horizontal");
            return Optional.empty();
        }

        Ruling expanded_horizontal = horizontal.expand(PERPENDICULAR_UNIT_EXPAND_AMOUNT);
        Ruling expanded_vertical = vertical.expand(PERPENDICULAR_UNIT_EXPAND_AMOUNT);

        if (!expanded_horizontal.intersectsLine(expanded_vertical)) {
            return Optional.empty();
        }

        return Optional.of(new Point2D.Float(vertical.getLeft(), horizontal.getTop()));
    }


    private class SweepStep implements Comparable<SweepStep> {

        protected Type type;
        protected float y_position;
        protected Ruling ruling;

        private enum Type {
            VERTICAL,
            HORIZONTAL_EXIT,
            HORIZONTAL_ENTRY
        }


        public SweepStep(Type type, float y_position, Ruling ruling) {

            this.type = type;
            this.y_position = y_position;
            this.ruling = ruling;
        }


        @Override
        public int compareTo(SweepStep other) {

            int rv;
            if (DoubleComparisons.feq(y_position, other.y_position)) {
                if (type == SweepStep.Type.VERTICAL && other.type == SweepStep.Type.HORIZONTAL_ENTRY) {
                    rv = 1;
                } else if (type == SweepStep.Type.VERTICAL && other.type == SweepStep.Type.HORIZONTAL_EXIT) {
                    rv = -1;
                } else if (type == SweepStep.Type.HORIZONTAL_ENTRY && other.type == SweepStep.Type.VERTICAL) {
                    rv = -1;
                } else if (type == SweepStep.Type.HORIZONTAL_EXIT && other.type == SweepStep.Type.VERTICAL) {
                    rv = 1;
                } else {
                    rv = Double.compare(y_position, other.y_position);
                }
            } else {
                return Double.compare(y_position, other.y_position);
            }
            return rv;
        }

    }

    public record IntersectingRulings(Ruling horizontal, Ruling vertical) {

    }

}
