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

import static com.knecon.fforesight.service.layoutparser.processor.utils.GeometricComparators.X_FIRST_RULING_COMPARATOR;

import java.awt.geom.Point2D;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

import org.springframework.stereotype.Service;

import com.knecon.fforesight.service.layoutparser.processor.model.table.CleanRulings;
import com.knecon.fforesight.service.layoutparser.processor.model.table.Rectangle;
import com.knecon.fforesight.service.layoutparser.processor.model.table.Ruling;
import com.knecon.fforesight.service.layoutparser.processor.python_api.model.table.TableCells;
import com.knecon.fforesight.service.layoutparser.processor.utils.UnionFind;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@Service
@RequiredArgsConstructor
public class RulingCleaningService {

    private static final float THRESHOLD_X_VERTICAL = 1;
    private static final float THRESHOLD_Y_VERTICAL = 2;
    private static final float THRESHOLD_X_HORIZONTAL = 2;
    private static final float THRESHOLD_Y_HORIZONTAL = 3;


    public CleanRulings getCleanRulings(List<TableCells> tableCells, List<Ruling> rulings) {

        Rulings verticalAndHorizontalRulingLines;

        if (!rulings.isEmpty()) {
            verticalAndHorizontalRulingLines = extractVerticalAndHorizontalRulingLines(rulings);
        } else {
            verticalAndHorizontalRulingLines = getRulingsFromParsedCells(tableCells);
        }

        verticalAndHorizontalRulingLines.verticalLines.sort(X_FIRST_RULING_COMPARATOR);
        verticalAndHorizontalRulingLines.horizontalLines.sort(X_FIRST_RULING_COMPARATOR);
        verticalAndHorizontalRulingLines = cleanRulings(verticalAndHorizontalRulingLines);

        return new CleanRulings(verticalAndHorizontalRulingLines.horizontalLines(), verticalAndHorizontalRulingLines.verticalLines());
    }


    private Rulings cleanRulings(Rulings rulings) {

        List<List<Rectangle>> groupedOverlappingVerticalRectangles = groupOverlappingRectangles(rulings.verticalLines.stream()
                                                                                                        .map(RulingCleaningService::getOverlapRectangle)
                                                                                                        .distinct()
                                                                                                        .toList());
        List<Ruling> cleanedVerticalRulings = groupedOverlappingVerticalRectangles.stream()
                .map(rectList -> getXCenteredRuling(Rectangle.boundingBoxOf(rectList)))
                .toList();

        List<List<Rectangle>> groupedOverlappingHorizontalRectangles = groupOverlappingRectangles(rulings.horizontalLines.stream()
                                                                                                          .map(RulingCleaningService::getOverlapRectangle)
                                                                                                          .distinct()
                                                                                                          .toList());

        List<Ruling> cleanedHorizontalRulings = groupedOverlappingHorizontalRectangles.stream()
                .map(rectList -> getYCenteredRuling(Rectangle.boundingBoxOf(rectList)))
                .collect(Collectors.toList());

        return new Rulings(cleanedVerticalRulings, cleanedHorizontalRulings);
    }


    private List<List<Rectangle>> groupOverlappingRectangles(List<Rectangle> rectangles) {

        UnionFind<Rectangle> unionFind = new UnionFind<>();
        for (int i = 0; i < rectangles.size(); i++) {
            for (int j = i + 1; j < rectangles.size(); j++) {
                Rectangle rectangle1 = rectangles.get(i);
                Rectangle rectangle2 = rectangles.get(j);

                // we can stop early when we are too far off because of x-y-sorting
                if(rectangle1.getRight() < rectangle2.getLeft() && rectangle1.getBottom() < rectangle2.getTop()) {
                    break;
                }

                if (rectangle1.intersects(rectangle2)) {
                    unionFind.union(rectangle1, rectangle2);
                }
            }
        }

        Map<Rectangle, List<Rectangle>> groups = new HashMap<>();
        for (Rectangle rectangle : rectangles) {
            Rectangle root = unionFind.find(rectangle);
            groups.computeIfAbsent(root, k -> new ArrayList<>()).add(rectangle);
        }
        return new ArrayList<>(groups.values());
    }


    private static Rectangle getOverlapRectangle(Ruling ruling) {

        float top;
        float left;
        float w;
        float h;

        if (ruling.x1 < ruling.x2) {
            left = ruling.x1;
            w = ruling.x2 - ruling.x1;
        } else {
            left = ruling.x2;
            w = ruling.x1 - ruling.x2;
        }
        if (ruling.y1 < ruling.y2) {
            top = ruling.y1;
            h = ruling.y2 - ruling.y1;
        } else {
            top = ruling.y2;
            h = ruling.y1 - ruling.y2;
        }

        if (ruling.isHorizontal()) {
            return new Rectangle(top - THRESHOLD_Y_HORIZONTAL, left - THRESHOLD_X_HORIZONTAL, w + 2 * THRESHOLD_X_HORIZONTAL, h + 2 * THRESHOLD_Y_HORIZONTAL);
        } else {
            return new Rectangle(top - THRESHOLD_Y_VERTICAL, left - THRESHOLD_X_VERTICAL, w + 2 * THRESHOLD_X_VERTICAL, h + 2 * THRESHOLD_Y_VERTICAL);
        }
    }


    public static Ruling getXCenteredRuling(Rectangle rectangle) {

        float x = (float) rectangle.getCenterX();
        float y1 = rectangle.getTop();
        float y2 = rectangle.getBottom();

        Point2D point1 = new Point2D.Float(x, y1 + THRESHOLD_Y_VERTICAL);
        Point2D point2 = new Point2D.Float(x, y2 - THRESHOLD_Y_VERTICAL);

        return new Ruling(point1, point2);
    }


    public static Ruling getYCenteredRuling(Rectangle rectangle) {

        float x1 = rectangle.getLeft();
        float x2 = rectangle.getRight();
        float y = (float) rectangle.getCenterY();

        Point2D point1 = new Point2D.Float(x1 + THRESHOLD_X_HORIZONTAL, y);
        Point2D point2 = new Point2D.Float(x2 - THRESHOLD_X_HORIZONTAL, y);

        return new Ruling(point1, point2);
    }


    private Rulings extractVerticalAndHorizontalRulingLines(List<Ruling> rulings) {

        List<Ruling> vrs = new ArrayList<>();
        for (Ruling vr : rulings) {
            if (vr.isVertical()) {
                vrs.add(vr);
            }
        }

        List<Ruling> hrs = new ArrayList<>();
        for (Ruling hr : rulings) {
            if (hr.isHorizontal()) {
                hrs.add(hr);
            }
        }
        return new Rulings(vrs, hrs);
    }


    private Rulings getRulingsFromParsedCells(List<TableCells> tableCells) {

        List<Ruling> vrs = extractVerticalRulingsFromParsedCells(tableCells);
        List<Ruling> hrs = extractHorizontalRulingsFromParsedCells(tableCells);
        return new Rulings(vrs, hrs);
    }


    private List<Ruling> extractVerticalRulingsFromParsedCells(List<TableCells> tableCells) {

        List<Ruling> vrs = new ArrayList<>();

        if (tableCells != null) {
            for (TableCells tableCell : tableCells) {
                Ruling leftLine = createRuling(tableCell.getX0(), tableCell.getX0(), tableCell.getY0(), tableCell.getY1());
                Ruling rightLine = createRuling(tableCell.getX1(), tableCell.getX1(), tableCell.getY0(), tableCell.getY1());
                vrs.add(leftLine);
                vrs.add(rightLine);
            }
        }
        return vrs;
    }


    private List<Ruling> extractHorizontalRulingsFromParsedCells(List<TableCells> tableCells) {

        List<Ruling> hrs = new ArrayList<>();

        if (tableCells != null) {
            for (TableCells tableCell : tableCells) {
                Ruling topLine = createRuling(tableCell.getX0(), tableCell.getX1(), tableCell.getY1(), tableCell.getY1());
                Ruling baseLine = createRuling(tableCell.getX0(), tableCell.getX1(), tableCell.getY0(), tableCell.getY0());
                hrs.add(topLine);
                hrs.add(baseLine);
            }
        }
        return hrs;
    }


    private Ruling createRuling(float tableCellX0, float tableCellX1, float tableCellY0, float tableCellY1) {

        float x0 = tableCellX0;
        float x1 = tableCellX1;
        float y0 = tableCellY0;
        float y1 = tableCellY1;

        if (x1 < x0) {
            x0 = tableCellX1;
            x1 = tableCellX0;
        }

        if (y1 < y0) {
            y0 = tableCellY1;
            y1 = tableCellY0;
        }

        return new Ruling(new Point2D.Float(x0, y0), new Point2D.Float(x1, y1));
    }


    private record Rulings(List<Ruling> verticalLines, List<Ruling> horizontalLines) {

    }

}
