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

import java.awt.geom.Rectangle2D;
import java.awt.geom.RectangularShape;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;
import java.util.stream.Stream;
import com.iqser.red.commons.jackson.ObjectMapperFactory;

import com.knecon.fforesight.service.layoutparser.processor.model.GapInformation;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.experimental.UtilityClass;

@UtilityClass
public class GapsAcrossLinesService {

    private static final double GAP_WIDTH_THRESHOLD_FACTOR = 0.01; // multiplied with avg text height
    private static final double LINE_COUNT_THRESHOLD_FACTOR = 0.3; // multiplied with average line count per page
    private static final double DISTANCE_TO_BORDER_THRESHOLD = 1;


    public List<Rectangle2D> detectXGapsAcrossLines(GapInformation gapInformation, Rectangle2D mainBodyTextFrame) {

        if (gapInformation.getXGaps().size() < 2) {
            return List.of(mainBodyTextFrame);
        }
        double avgHeight = gapInformation.getXGaps()
                .stream()
                .filter(gaps -> !gaps.isEmpty())
                .map(gaps -> gaps.get(0))
                .mapToDouble(RectangularShape::getHeight)
                .average()
                .orElseThrow();

        ColumnFactory columnFactory = ColumnFactory.init(avgHeight, gapInformation.getXGaps().size());
        gapInformation.getXGaps().get(0).stream().map(GapAcrossLines::new).forEach(columnFactory::addToQueue);
        List<List<Rectangle2D>> xGaps = gapInformation.getXGaps();
        for (var gaps : xGaps.subList(1, xGaps.size())) {

            while (columnFactory.hasGapsToProcess()) {
                GapAcrossLines gapAcrossLines = columnFactory.getNext();
                rememberColumnIfValid(columnFactory, gapAcrossLines);
                elongateColumnsAndFilterForWidth(gapAcrossLines, gaps, columnFactory).forEach(columnFactory::setToStillInProgress);
            }
            columnFactory.addStillInProgressToQueue();
            columnFactory.addGapsToQueue(gaps);
        }

        return columnFactory.outputGaps.stream()
                .filter(gapAcrossLines -> columnFactory.outputGaps.stream().filter(gapAcrossLines::intersectsX).noneMatch(gapAcrossLines1 -> gapAcrossLines1.lineCount > gapAcrossLines.lineCount))
                .filter(gapAcrossLines -> Math.abs(gapAcrossLines.rectangle2D.getMinX() - mainBodyTextFrame.getMinX()) > DISTANCE_TO_BORDER_THRESHOLD)
                .filter(gapAcrossLines -> Math.abs(gapAcrossLines.rectangle2D.getMaxX() - mainBodyTextFrame.getMaxX()) > DISTANCE_TO_BORDER_THRESHOLD)
                .map(GapAcrossLines::getRectangle2D)
                .toList();
    }


    private static void rememberColumnIfValid(ColumnFactory columnFactory, GapAcrossLines gapAcrossLines) {

        if (gapAcrossLines.lineCount >= LINE_COUNT_THRESHOLD_FACTOR * (double) columnFactory.lineCount) {
            columnFactory.outputGaps.add(gapAcrossLines);
        }
    }


    private static Stream<GapAcrossLines> elongateColumnsAndFilterForWidth(GapAcrossLines gapAcrossLines, List<Rectangle2D> gaps, ColumnFactory columnFactory) {

        return gaps.stream()//
                .filter(gap -> gapAcrossLines.getIntersectionWidth(gap) > GAP_WIDTH_THRESHOLD_FACTOR * columnFactory.avgHeight)//
                .map(gapAcrossLines::addNewLineAndShrink);

    }


    private static Rectangle2D correctRectangle(Rectangle2D rectangle2D) {

        double minX = Math.min(rectangle2D.getMinX(), rectangle2D.getMaxX());
        double minY = Math.min(rectangle2D.getMinY(), rectangle2D.getMaxY());
        double maxX = Math.max(rectangle2D.getMinX(), rectangle2D.getMaxX());
        double maxY = Math.max(rectangle2D.getMinY(), rectangle2D.getMaxY());
        return new Rectangle2D.Double(minX, minY, maxX - minX, maxY - minY);
    }


    @Getter
    @AllArgsConstructor
    private class GapAcrossLines {

        Rectangle2D rectangle2D;
        int lineCount = 1;


        GapAcrossLines(Rectangle2D rectangle2D) {

            this.rectangle2D = correctRectangle(rectangle2D);
        }


        public boolean intersectsX(Rectangle2D rectangle2D) {

            return rectangle2D.getMinX() < this.rectangle2D.getMaxX() && this.rectangle2D.getMinX() < rectangle2D.getMaxX();
        }


        public boolean intersectsX(GapAcrossLines gapAcrossLines) {

            return this.intersectsX(gapAcrossLines.getRectangle2D());
        }


        public double getIntersectionWidth(Rectangle2D rectangle2D) {

            if (!intersectsX(rectangle2D)) {
                return -1;
            }
            double min_x = Math.max(rectangle2D.getMinX(), this.rectangle2D.getMinX());
            double max_x = Math.min(rectangle2D.getMaxX(), this.rectangle2D.getMaxX());
            return max_x - min_x;
        }


        public GapAcrossLines addNewLineAndShrink(Rectangle2D rectangle2D) {

            var correctedRectangle = correctRectangle(rectangle2D);
            double min_x = Math.max(correctedRectangle.getMinX(), this.rectangle2D.getMinX());
            double max_x = Math.min(correctedRectangle.getMaxX(), this.rectangle2D.getMaxX());
            double min_y = correctedRectangle.getMinY();
            double max_y = this.rectangle2D.getMaxY();
            double width = max_x - min_x;
            double height = max_y - min_y;
            return new GapAcrossLines(new Rectangle2D.Double(min_x, min_y, width, height), lineCount + 1);
        }

    }

    @RequiredArgsConstructor
    private class ColumnFactory {

        final double avgHeight;
        final int lineCount;

        List<GapAcrossLines> outputGaps = new LinkedList<>();
        Queue<GapAcrossLines> gapsQueue = new LinkedList<>();
        List<GapAcrossLines> gapsToQueue = new LinkedList<>();


        public static ColumnFactory init(double avgHeight, int lineCount) {

            return new ColumnFactory(Math.abs(avgHeight), lineCount);
        }


        public GapAcrossLines getNext() {

            return gapsQueue.remove();
        }


        public void addToQueue(GapAcrossLines gapAcrossLines) {

            gapsQueue.add(gapAcrossLines);
        }


        public void addToQueue(Rectangle2D gap) {

            gapsQueue.add(new GapAcrossLines(gap));
        }


        private boolean hasGapsToProcess() {

            return gapsQueue.peek() != null;
        }


        public void setToStillInProgress(GapAcrossLines gapAcrossLines) {

            gapsToQueue.add(gapAcrossLines);
        }


        private void addStillInProgressToQueue() {

            for (int i = gapsToQueue.size() - 1; i >= 0; i--) {
                gapsQueue.add(gapsToQueue.remove(i));
            }
        }


        public void addGapsToQueue(List<Rectangle2D> gaps) {

            gaps.forEach(this::addToQueue);
        }

    }

}
