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

import java.awt.geom.Rectangle2D;
import java.util.LinkedList;
import java.util.List;

import com.knecon.fforesight.service.layoutparser.processor.model.GapInformation;
import com.knecon.fforesight.service.layoutparser.processor.model.text.TextPositionSequence;
import com.knecon.fforesight.service.layoutparser.processor.utils.RectangleTransformations;

import lombok.AllArgsConstructor;
import lombok.experimental.UtilityClass;

@UtilityClass
public class GapDetectionService {

    private static final double X_GAP_FACTOR = 1; // multiplied with average text height, determines the minimum distance of gaps in lines
    private static final double Y_GAP_FACTOR = 1;
    private static final double NEW_LINE_FACTOR = 0.2;


    public static GapInformation findGapsInLines(List<TextPositionSequence> sortedTextPositionSequences, Rectangle2D mainBodyTextFrame) {

        if (sortedTextPositionSequences.isEmpty()) {
            return new GapInformation();
        }

        final double avgTextPositionHeight = getAvgTextPositionHeight(sortedTextPositionSequences);

        XGapsContext xGapContext = XGapsContext.init(mainBodyTextFrame);
        YGapsContext yGapContext = YGapsContext.init(mainBodyTextFrame);

        var previousTextPosition = sortedTextPositionSequences.get(0);
        Rectangle2D rectangle = toRectangle2D(previousTextPosition);

        xGapContext.addGapFromLeftEdgeOfMainBody(rectangle);

        for (TextPositionSequence currentTextPosition : sortedTextPositionSequences.subList(1, sortedTextPositionSequences.size())) {

            double yDifference = Math.abs(currentTextPosition.getMaxYDirAdj() - previousTextPosition.getMaxYDirAdj());
            double xGap = Math.abs(previousTextPosition.getMaxXDirAdj() - currentTextPosition.getMinXDirAdj());
            Rectangle2D previousTextPositionBBox = toRectangle2D(previousTextPosition);
            Rectangle2D currentTextPositionBBox = toRectangle2D(currentTextPosition);

            if (yDifference > avgTextPositionHeight * Y_GAP_FACTOR) {
                yGapContext.addGap(mainBodyTextFrame.getMinX(),
                                   previousTextPositionBBox.getMaxY(),
                                   mainBodyTextFrame.getWidth(),
                                   -(previousTextPositionBBox.getMaxY() - currentTextPositionBBox.getMinY()));
            }
            if (yDifference > avgTextPositionHeight * NEW_LINE_FACTOR) {

                xGapContext.addGapToRightEdgeOfMainBody(previousTextPositionBBox);
                xGapContext.gapsInCurrentLine = new LinkedList<>();
                xGapContext.gapsPerLine.add(xGapContext.gapsInCurrentLine);
                xGapContext.addGapFromLeftEdgeOfMainBody(currentTextPositionBBox);

            } else if (xGap > avgTextPositionHeight * X_GAP_FACTOR) {
                addGapToLine(currentTextPositionBBox, previousTextPositionBBox, xGapContext);
            }
            previousTextPosition = currentTextPosition;
        }
        xGapContext.addGapToRightEdgeOfMainBody(toRectangle2D(sortedTextPositionSequences.get(sortedTextPositionSequences.size() - 1)));
        xGapContext.gapsPerLine.add(xGapContext.gapsInCurrentLine);

        return new GapInformation(xGapContext.gapsPerLine, yGapContext.gapsPerLine);
    }


    private static Rectangle2D toRectangle2D(TextPositionSequence textPosition) {

        return mirrorY(RectangleTransformations.toRectangle2D(textPosition.getRectangle()));
    }


    private static Rectangle2D mirrorY(Rectangle2D rectangle2D) {

        return new Rectangle2D.Double(rectangle2D.getX(), Math.min(rectangle2D.getMinY(), rectangle2D.getMaxY()), rectangle2D.getWidth(), Math.abs(rectangle2D.getHeight()));
    }


    private static void addGapToLine(Rectangle2D currentTextPosition, Rectangle2D previousTextPosition, XGapsContext context) {

        context.gapsInCurrentLine.add(new Rectangle2D.Double(previousTextPosition.getMaxX(),
                                                             previousTextPosition.getMinY(),
                                                             currentTextPosition.getMinX() - previousTextPosition.getMaxX(),
                                                             (previousTextPosition.getHeight() + currentTextPosition.getHeight()) / 2));
    }


    private static void assertAllTextPositionsHaveSameDir(List<TextPositionSequence> textPositionSequences) {

        assert textPositionSequences.stream()
                .map(TextPositionSequence::getDir)
                .allMatch(a -> a.equals(textPositionSequences.get(0).getDir()));
    }


    private static double getAvgTextPositionHeight(List<TextPositionSequence> textPositionSequences) {

        return textPositionSequences.stream()
                .mapToDouble(TextPositionSequence::getHeight).average().orElseThrow();
    }


    @AllArgsConstructor
    private static class YGapsContext {

        List<List<Rectangle2D>> gapsPerLine;
        List<Rectangle2D> gapsInCurrentLine;
        Rectangle2D mainBodyTextFrame;


        public static YGapsContext init(Rectangle2D mainBodyTextFrame) {

            List<List<Rectangle2D>> initialLinesWithGaps = new LinkedList<>();
            List<Rectangle2D> initialBlocksInLine = new LinkedList<>();
            initialLinesWithGaps.add(initialBlocksInLine);
            return new YGapsContext(initialLinesWithGaps, initialBlocksInLine, mainBodyTextFrame);
        }


        public void addGap(double x1, double y1, double w, double h) {

            gapsInCurrentLine.add(new Rectangle2D.Double(x1, y1, w, h));
        }

    }

    @AllArgsConstructor
    private static class XGapsContext {

        List<List<Rectangle2D>> gapsPerLine;
        List<Rectangle2D> gapsInCurrentLine;
        Rectangle2D mainBodyTextFrame;


        public static XGapsContext init(Rectangle2D mainBodyTextFrame) {

            List<List<Rectangle2D>> initialLinesWithGaps = new LinkedList<>();
            List<Rectangle2D> initialBlocksInLine = new LinkedList<>();
            initialLinesWithGaps.add(initialBlocksInLine);
            return new XGapsContext(initialLinesWithGaps, initialBlocksInLine, mainBodyTextFrame);
        }


        public void addGapToRightEdgeOfMainBody(Rectangle2D textPosition) {

            Rectangle2D leftGap = new Rectangle2D.Double(textPosition.getMaxX(),
                                                         textPosition.getMinY(),
                                                         mainBodyTextFrame.getMaxX() - textPosition.getMaxX(),
                                                         textPosition.getHeight());
            gapsInCurrentLine.add(leftGap);
        }


        public void addGapFromLeftEdgeOfMainBody(Rectangle2D textPosition) {

            Rectangle2D leftGap = new Rectangle2D.Double(mainBodyTextFrame.getMinX(),
                                                         textPosition.getMinY(),
                                                         textPosition.getMinX() - mainBodyTextFrame.getMinX(),
                                                         textPosition.getHeight());
            gapsInCurrentLine.add(leftGap);
        }

    }

}
