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

import static java.lang.String.format;

import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.awt.geom.RectangularShape;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import java.util.function.BiConsumer;
import java.util.function.BinaryOperator;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collector;

import org.apache.pdfbox.pdmodel.common.PDRectangle;

import com.iqser.red.service.persistence.service.v1.api.shared.model.redactionlog.Point;
import com.iqser.red.service.persistence.service.v1.api.shared.model.redactionlog.Rectangle;
import com.knecon.fforesight.service.layoutparser.processor.model.graph.textblock.AtomicTextBlock;
import com.knecon.fforesight.service.layoutparser.processor.model.table.CleanRulings;
import com.knecon.fforesight.service.layoutparser.processor.model.table.Ruling;

import lombok.AllArgsConstructor;
import lombok.NoArgsConstructor;

public class RectangleTransformations {

    public static Rectangle2D pad(Rectangle2D rectangle2D, int deltaX, int deltaY) {

        return new Rectangle2D.Double(rectangle2D.getMinX() - deltaX, rectangle2D.getMinY() - deltaY, rectangle2D.getWidth() + 2 * deltaX, rectangle2D.getHeight() + 2 * deltaY);
    }


    public static Rectangle2D pad(Rectangle2D rectangle2D, double deltaX, double deltaY) {

        return new Rectangle2D.Double(rectangle2D.getMinX() - deltaX, rectangle2D.getMinY() - deltaY, rectangle2D.getWidth() + 2 * deltaX, rectangle2D.getHeight() + 2 * deltaY);
    }


    public static double calculateIntersectedArea(Rectangle2D r1, Rectangle2D r2) {

        double xOverlap = Math.max(0, Math.min(r1.getMaxX(), r2.getMaxX()) - Math.max(r1.getMinX(), r2.getMinX()));
        double yOverlap = Math.max(0, Math.min(r1.getMaxY(), r2.getMaxY()) - Math.max(r1.getY(), r2.getY()));

        return xOverlap * yOverlap;
    }


    public static Rectangle2D bBoxUnionAtomicTextBlock(List<AtomicTextBlock> atomicTextBlocks) {

        return atomicTextBlocks.stream()
                .flatMap(atomicTextBlock -> atomicTextBlock.getPositions()
                        .stream())
                .collect(new Rectangle2DBBoxCollector());
    }


    public static Collector<Rectangle2D, Rectangle2DBBoxCollector.BBox, Rectangle2D> collectBBox() {

        return new Rectangle2DBBoxCollector();
    }


    public static PDRectangle toPDRectangleBBox(List<Rectangle> rectangles) {

        Rectangle2D rectangle2D = RectangleTransformations.rectangleBBox(rectangles);

        PDRectangle annotationPosition = new PDRectangle();
        annotationPosition.setLowerLeftX((float) rectangle2D.getMinX());
        annotationPosition.setLowerLeftY((float) rectangle2D.getMinY());
        annotationPosition.setUpperRightX((float) rectangle2D.getMaxX());
        annotationPosition.setUpperRightY((float) rectangle2D.getMaxY());
        return annotationPosition;
    }


    public static Rectangle2D atomicTextBlockBBox(List<AtomicTextBlock> atomicTextBlocks) {

        return atomicTextBlocks.stream()
                .flatMap(atomicTextBlock -> atomicTextBlock.getPositions()
                        .stream())
                .collect(new Rectangle2DBBoxCollector());
    }


    public static String toString(Rectangle2D rectangle2D) {

        return format("%f,%f,%f,%f", rectangle2D.getX(), rectangle2D.getY(), rectangle2D.getWidth(), rectangle2D.getHeight());
    }


    public static Rectangle2D rectangleBBox(List<Rectangle> rectangles) {

        return rectangles.stream()
                .map(RectangleTransformations::toRectangle2D)
                .collect(new Rectangle2DBBoxCollector());
    }


    public static Rectangle2D toRectangle2D(Rectangle redactionLogRectangle) {

        return new Rectangle2D.Double(redactionLogRectangle.getTopLeft().getX(),
                                      redactionLogRectangle.getTopLeft().getY() + redactionLogRectangle.getHeight(),
                                      redactionLogRectangle.getWidth(),
                                      -redactionLogRectangle.getHeight());
    }


    public static Rectangle2D toRectangle2D(PDRectangle rectangle) {

        return new Rectangle2D.Double(rectangle.getLowerLeftX(), rectangle.getLowerLeftY(), rectangle.getWidth(), rectangle.getHeight());
    }


    public static Rectangle toRedactionLogRectangle(Rectangle2D rectangle2D, int pageNumber) {

        return new Rectangle(new Point((float) rectangle2D.getMinX(), (float) (rectangle2D.getMinY() + rectangle2D.getHeight())),
                             (float) rectangle2D.getWidth(),
                             -(float) rectangle2D.getHeight(),
                             pageNumber);
    }


    public static Rectangle2D rectangle2DBBox(List<Rectangle2D> rectangle2DList) {

        return rectangle2DList.stream()
                .collect(new Rectangle2DBBoxCollector());
    }


    /**
     * If two rectangles are further apart than five times the average width of a rectangle, a gap is inserted.
     *
     * @param rectangle2DList A list of rectangles to combine
     * @return A list of rectangles which are combined if they are closer than the split threshold
     */
    public static List<Rectangle2D> rectangleBBoxWithGaps(List<Rectangle2D> rectangle2DList) {

        if (rectangle2DList.isEmpty()) {
            return Collections.emptyList();
        }
        double splitThreshold = rectangle2DList.stream()
                                        .mapToDouble(RectangularShape::getWidth).average().orElse(5) * 5.0;

        List<List<Rectangle2D>> rectangleListsWithGaps = new LinkedList<>();
        List<Rectangle2D> rectangleListWithoutGaps = new LinkedList<>();
        rectangleListsWithGaps.add(rectangleListWithoutGaps);
        Rectangle2D previousRectangle = rectangle2DList.get(0);
        for (Rectangle2D currentRectangle : rectangle2DList) {
            if (Math.abs(currentRectangle.getMinX() - previousRectangle.getMaxX()) > splitThreshold) {
                rectangleListWithoutGaps = new LinkedList<>();
                rectangleListWithoutGaps.add(currentRectangle);
                rectangleListsWithGaps.add(rectangleListWithoutGaps);
                previousRectangle = currentRectangle;
            } else {
                rectangleListWithoutGaps.add(currentRectangle);
                previousRectangle = currentRectangle;
            }
        }
        return rectangleListsWithGaps.stream()
                .map(RectangleTransformations::rectangle2DBBox)
                .toList();
    }


    public static CleanRulings extractRulings(List<? extends Rectangle2D.Float> rectangles) {

        // Underlined or strikethrough are also in rulings but we dont want to split blocks with them so we use cells.
        List<Ruling> horizontalRulings = new ArrayList<>();
        List<Ruling> verticalRulings = new ArrayList<>();

        rectangles.forEach(rectangle -> {
            horizontalRulings.add(new Ruling(new Point2D.Float(rectangle.x, rectangle.y), new Point2D.Float(rectangle.x + rectangle.width, rectangle.y)));
            horizontalRulings.add(new Ruling(new Point2D.Float(rectangle.x, rectangle.y + rectangle.height),
                                             new Point2D.Float(rectangle.x + rectangle.width, rectangle.y + rectangle.height)));
            verticalRulings.add(new Ruling(new Point2D.Float(rectangle.x, rectangle.y), new Point2D.Float(rectangle.x, rectangle.y + rectangle.height)));
            verticalRulings.add(new Ruling(new Point2D.Float(rectangle.x + rectangle.width, rectangle.y),
                                           new Point2D.Float(rectangle.x + rectangle.width, rectangle.y + rectangle.height)));
        });
        return new CleanRulings(verticalRulings, horizontalRulings);
    }


    private static class Rectangle2DBBoxCollector implements Collector<Rectangle2D, Rectangle2DBBoxCollector.BBox, Rectangle2D> {

        @Override
        public Supplier<BBox> supplier() {

            return BBox::new;
        }


        @Override
        public BiConsumer<BBox, Rectangle2D> accumulator() {

            return BBox::addRectangle;
        }


        @Override
        public BinaryOperator<BBox> combiner() {

            return (b1, b2) -> new BBox(Math.min(b1.lowerLeftX, b2.lowerLeftX),
                                        Math.min(b1.lowerLeftY, b2.lowerLeftY),
                                        Math.max(b1.upperRightX, b2.upperRightX),
                                        Math.max(b1.upperRightY, b2.upperRightY));
        }


        @Override
        public Function<BBox, Rectangle2D> finisher() {

            return BBox::toRectangle2D;
        }


        @Override
        public Set<Characteristics> characteristics() {

            return Set.of(Characteristics.UNORDERED);
        }


        @AllArgsConstructor
        @NoArgsConstructor
        private static class BBox {

            Double lowerLeftX;
            Double lowerLeftY;
            Double upperRightX;
            Double upperRightY;


            public Rectangle2D toRectangle2D() {

                if (lowerLeftX == null || lowerLeftY == null || upperRightX == null || upperRightY == null) {
                    return new Rectangle2D.Double(0, 0, 0, 0);
                }
                return new Rectangle2D.Double(lowerLeftX, lowerLeftY, upperRightX - lowerLeftX, upperRightY - lowerLeftY);
            }


            public void addRectangle(Rectangle2D rectangle2D) {

                double lowerLeftX = Math.min(rectangle2D.getMinX(), rectangle2D.getMaxX());
                double lowerLeftY = Math.min(rectangle2D.getMinY(), rectangle2D.getMaxY());
                double upperRightX = Math.max(rectangle2D.getMinX(), rectangle2D.getMaxX());
                double upperRightY = Math.max(rectangle2D.getMinY(), rectangle2D.getMaxY());

                if (this.lowerLeftX == null) {
                    this.lowerLeftX = lowerLeftX;
                } else if (this.lowerLeftX > lowerLeftX) {
                    this.lowerLeftX = lowerLeftX;
                }
                if (this.lowerLeftY == null) {
                    this.lowerLeftY = lowerLeftY;
                } else if (this.lowerLeftY > lowerLeftY) {
                    this.lowerLeftY = lowerLeftY;
                }
                if (this.upperRightX == null) {
                    this.upperRightX = upperRightX;
                } else if (this.upperRightX < upperRightX) {
                    this.upperRightX = upperRightX;
                }
                if (this.upperRightY == null) {
                    this.upperRightY = upperRightY;
                } else if (this.upperRightY < upperRightY) {
                    this.upperRightY = upperRightY;
                }

            }

        }

    }

}