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.LineInformation;
import com.knecon.fforesight.service.layoutparser.processor.utils.RectangleTransformations;
import com.knecon.fforesight.service.layoutparser.processor.model.text.TextPositionSequence;

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

@UtilityClass
public class LineDetectionService {

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


    public LineInformation calculateLineInformation(List<TextPositionSequence> sortedTextPositionSequences) {

        if (sortedTextPositionSequences.isEmpty()) {
            return LineFactory.init().build();
        }

        return buildLineInformation(sortedTextPositionSequences);
    }


    public List<List<Rectangle2D>> findLinesWithGaps(List<TextPositionSequence> sortedTextPositionSequences, Rectangle2D mainBodyTextFrame) {

        return calculateLineInformation(sortedTextPositionSequences).getBBoxWithGapsByLines();
    }


    public List<List<TextPositionSequence>> orderByLines(List<TextPositionSequence> sortedTextPositionSequences, Rectangle2D mainBodyTextFrame) {

        return calculateLineInformation(sortedTextPositionSequences).getSequencesByLines();
    }


    private static LineInformation buildLineInformation(List<TextPositionSequence> sortedTextPositionSequences) {

        final double avgTextPositionHeight = getAvgTextPositionHeight(sortedTextPositionSequences);

        LineFactory lineFactory = LineFactory.init();

        var previousTextPosition = sortedTextPositionSequences.get(0);
        lineFactory.addToCurrentLine(previousTextPosition);
        for (TextPositionSequence currentTextPosition : sortedTextPositionSequences.subList(1, sortedTextPositionSequences.size())) {
            if (isNewLine(currentTextPosition, previousTextPosition, avgTextPositionHeight) || isSplitByOrientation(currentTextPosition, previousTextPosition)) {
                lineFactory.startNewLine();
            } else if (isXGap(currentTextPosition, previousTextPosition, avgTextPositionHeight)) {
                lineFactory.startNewBlock();
            }
            lineFactory.addToCurrentLine(currentTextPosition);
            previousTextPosition = currentTextPosition;
        }
        lineFactory.addFinalLine();
        return lineFactory.build();
    }


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

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


    private static boolean isXGap(TextPositionSequence currentTextPosition, TextPositionSequence previousTextPosition, double avgTextPositionHeight) {

        return Math.abs(previousTextPosition.getMaxXDirAdj() - currentTextPosition.getMinXDirAdj()) > (avgTextPositionHeight * X_GAP_FACTOR);
    }


    private static boolean isSplitByOrientation(TextPositionSequence currentTextPosition, TextPositionSequence previousTextPosition) {

        return !previousTextPosition.getDir().equals(currentTextPosition.getDir());
    }


    private static boolean isNewLine(TextPositionSequence currentTextPosition, TextPositionSequence previousTextPosition, double avgTextPositionHeight) {

        return Math.abs(previousTextPosition.getMinYDirAdj() - currentTextPosition.getMinYDirAdj()) > avgTextPositionHeight;
    }


    @Getter
    @AllArgsConstructor
    private class LineFactory {

        List<Rectangle2D> lineBBox;

        List<List<Rectangle2D>> bBoxWithGapsByLines;
        List<Rectangle2D> bBoxWithGapsInCurrentLine;

        List<List<List<TextPositionSequence>>> sequencesWithGapsByLines;
        List<List<TextPositionSequence>> sequencesWithGapsInCurrentLine;

        List<TextPositionSequence> currentSequencesWithoutGaps;

        List<List<TextPositionSequence>> sequencesByLines;
        List<TextPositionSequence> sequencesInCurrentLine;

        List<List<Rectangle2D>> xGaps;
        List<List<Rectangle2D>> yGaps;


        public static LineFactory init() {

            List<Rectangle2D> lineBBox = new LinkedList<>();

            List<List<Rectangle2D>> bBoxWithGapsByLines = new LinkedList<>();
            List<Rectangle2D> bBoxWithGapsInCurrentLine = new LinkedList<>();
            bBoxWithGapsByLines.add(bBoxWithGapsInCurrentLine);

            List<List<List<TextPositionSequence>>> sequencesWithGapsByLines = new LinkedList<>();
            List<List<TextPositionSequence>> sequencesWithGapsInCurrentLine = new LinkedList<>();
            sequencesWithGapsByLines.add(sequencesWithGapsInCurrentLine);
            List<TextPositionSequence> currentSequencesWithoutGaps = new LinkedList<>();
            sequencesWithGapsInCurrentLine.add(currentSequencesWithoutGaps);

            List<List<TextPositionSequence>> sequencesByLines = new LinkedList<>();
            List<TextPositionSequence> sequencesInCurrentLine = new LinkedList<>();
            sequencesByLines.add(sequencesInCurrentLine);

            return new LineFactory(lineBBox,
                    bBoxWithGapsByLines,
                    bBoxWithGapsInCurrentLine,
                    sequencesWithGapsByLines,
                    sequencesWithGapsInCurrentLine,
                    currentSequencesWithoutGaps,
                    sequencesByLines,
                    sequencesInCurrentLine,
                    null,
                    null);
        }


        public void addGaps(GapInformation gapInformation) {

            this.xGaps = gapInformation.getXGaps();
            this.yGaps = gapInformation.getYGaps();
        }


        public LineInformation build() {

            return new LineInformation(lineBBox, sequencesByLines, bBoxWithGapsByLines, sequencesWithGapsByLines);
        }


        public void startNewBlock() {

            bBoxWithGapsInCurrentLine.add(textPositionBBox(currentSequencesWithoutGaps));
            currentSequencesWithoutGaps = new LinkedList<>();
            sequencesWithGapsInCurrentLine.add(currentSequencesWithoutGaps);
        }


        public void startNewLine() {

            lineBBox.add(textPositionBBox(sequencesInCurrentLine));

            bBoxWithGapsInCurrentLine.add(textPositionBBox(currentSequencesWithoutGaps));
            bBoxWithGapsInCurrentLine = new LinkedList<>();
            bBoxWithGapsByLines.add(bBoxWithGapsInCurrentLine);

            sequencesWithGapsInCurrentLine = new LinkedList<>();
            sequencesWithGapsByLines.add(sequencesWithGapsInCurrentLine);
            currentSequencesWithoutGaps = new LinkedList<>();
            sequencesWithGapsInCurrentLine.add(currentSequencesWithoutGaps);

            sequencesInCurrentLine = new LinkedList<>();
            sequencesByLines.add(sequencesInCurrentLine);
        }


        private Rectangle2D textPositionBBox(List<TextPositionSequence> textPositionSequences) {

            return RectangleTransformations.rectangleBBox(textPositionSequences.stream().map(TextPositionSequence::getRectangle).toList());
        }


        public void addToCurrentLine(TextPositionSequence current) {

            sequencesInCurrentLine.add(current);
            currentSequencesWithoutGaps.add(current);
        }


        public void addFinalLine() {

            lineBBox.add(textPositionBBox(sequencesInCurrentLine));
            bBoxWithGapsInCurrentLine.add(textPositionBBox(currentSequencesWithoutGaps));
        }

    }

}
