package com.knecon.fforesight.service.layoutparser.processor.model.table;

import java.awt.geom.Line2D;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Stream;

import com.knecon.fforesight.service.layoutparser.processor.docstrum.model.Character;

import lombok.Getter;

@Getter
public class CleanRulings {

    List<Ruling> horizontals; // unmodifiable sorted by Y list
    List<Ruling> verticals; // unmodifiable sorted by X list


    public CleanRulings(List<Ruling> horizontals, List<Ruling> verticals) {

        this.horizontals = horizontals.stream()
                .peek(Ruling::assertHorizontal)
                .sorted(Comparator.comparing(Line2D.Float::getY1))
                .toList();
        this.verticals = verticals.stream()
                .peek(Ruling::assertVertical)
                .sorted(Comparator.comparing(Line2D.Float::getX1))
                .toList();
    }


    public CleanRulings getTableLines() {

        return new CleanRulings(horizontals.stream()
                                        .filter(ruling -> ruling.getClassification().equals(Ruling.Classification.TABLE_LINE))
                                        .toList(),
                                verticals.stream()
                                        .filter(ruling -> ruling.getClassification().equals(Ruling.Classification.TABLE_LINE))
                                        .toList());
    }


    public CleanRulings withoutTextRulings() {

        return new CleanRulings(horizontals.stream()
                                        .filter(ruling -> !(ruling.getClassification().equals(Ruling.Classification.UNDERLINE) || ruling.getClassification()
                                                .equals(Ruling.Classification.STRIKETROUGH)))
                                        .toList(),
                                verticals.stream()
                                        .filter(ruling -> !(ruling.getClassification().equals(Ruling.Classification.UNDERLINE) || ruling.getClassification()
                                                .equals(Ruling.Classification.STRIKETROUGH)))
                                        .toList());
    }


    public List<Ruling> buildAll() {

        ArrayList<Ruling> rulings = new ArrayList<>(horizontals.size() + verticals.size());
        rulings.addAll(horizontals);
        rulings.addAll(verticals);
        return rulings;
    }


    public boolean lineBetween(Character a, Character b) {

        return lineBetween(a.getTextPosition().getInitialUserSpacePosition(), b.getTextPosition().getInitialUserSpacePosition());
    }


    public boolean lineBetween(Rectangle2D a, Rectangle2D b) {

        if (a.intersects(b)) {
            return false;
        }
        return lineBetween(new Point2D.Double(a.getCenterX(), a.getCenterY()), new Point2D.Double(b.getCenterX(), b.getCenterY()));
    }


    public boolean lineBetween(Point2D p1, Point2D p2) {

        Ruling ruling = new Ruling(p1, p2);

        if (ruling.isHorizontal()) {
            return getVerticalsInXInterval(ruling.x1, ruling.x2).stream()
                    .anyMatch(vertical -> vertical.intersectsLine(ruling));

        }

        if (ruling.isVertical()) {
            return getHorizontalsInYInterval(ruling.y1, ruling.y2).stream()
                    .anyMatch(horizontal -> horizontal.intersectsLine(ruling));

        }

        return Stream.of(getVerticalsInXInterval(ruling.x1, ruling.x2), getHorizontalsInYInterval(ruling.y1, ruling.y2))
                .flatMap(Collection::stream)
                .anyMatch(other -> other.intersectsLine(ruling));
    }


    public List<Ruling> getHorizontalsInYInterval(float y1, float y2) {

        float startY = Math.min(y1, y2);
        float endY = Math.max(y1, y2);

        if (horizontals.isEmpty() || Float.isNaN(startY) || Float.isNaN(endY)) {
            return Collections.emptyList();
        }

        int firstGreaterThanIdx = findFirstHorizontalRulingIdxAbove(startY);

        if (firstGreaterThanIdx == -1) {
            return Collections.emptyList();
        }

        List<Ruling> result = new ArrayList<>();
        for (int i = firstGreaterThanIdx; i < horizontals.size(); i++) {
            Ruling horizontal = horizontals.get(i);
            if (horizontal.y1 > endY) {
                break;
            }
            result.add(horizontal);
        }
        return result;
    }


    private int findFirstHorizontalRulingIdxAbove(float y) {

        int low = 0;
        int high = horizontals.size() - 1;

        while (low <= high) {
            int mid = low + (high - low) / 2;
            Line2D.Float midLine = horizontals.get(mid);
            float midY = midLine.y1;

            if (midY == y) {
                return mid;
            } else if (midY > y) {
                high = mid - 1;
            } else {
                low = mid + 1;
            }
        }

        // Return the index of the first element greater than y or -1 if not found
        return horizontals.size() > low && horizontals.get(low).y1 > y ? low : -1;
    }


    public List<Ruling> getVerticalsInXInterval(float x1, float x2) {

        float startX = Math.min(x1, x2);
        float endX = Math.max(x1, x2);

        if (verticals.isEmpty() || Float.isNaN(startX) || Float.isNaN(endX)) {
            return Collections.emptyList();
        }

        int firstGreaterThanIdx = findFirstVerticalRulingIdxRightOf(startX);

        if (firstGreaterThanIdx == -1) {
            return Collections.emptyList();
        }

        List<Ruling> result = new ArrayList<>();
        for (int i = firstGreaterThanIdx; i < verticals.size(); i++) {
            Ruling horizontal = verticals.get(i);
            if (horizontal.x1 > endX) {
                break;
            }
            result.add(horizontal);
        }
        return result;
    }


    private int findFirstVerticalRulingIdxRightOf(float x) {

        int low = 0;
        int high = verticals.size() - 1;

        while (low <= high) {
            int mid = low + (high - low) / 2;
            Line2D.Float midLine = verticals.get(mid);
            float midX = midLine.x1;

            if (midX == x) {
                return mid;
            } else if (midX > x) {
                high = mid - 1;
            } else {
                low = mid + 1;
            }
        }

        // Return the index of the first element greater than y or -1 if not found
        return verticals.size() > low && verticals.get(low).x1 > x ? low : -1;
    }

}
