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.Word;

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<Word> sortedWords) {

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

        return buildLineInformation(sortedWords);
    }


    public List<List<Rectangle2D>> findLinesWithGaps(List<Word> sortedWords, Rectangle2D mainBodyTextFrame) {

        return calculateLineInformation(sortedWords).getBBoxWithGapsByLines();
    }


    public List<List<Word>> orderByLines(List<Word> sortedWords, Rectangle2D mainBodyTextFrame) {

        return calculateLineInformation(sortedWords).getSequencesByLines();
    }


    private static LineInformation buildLineInformation(List<Word> sortedWords) {

        final double avgTextPositionHeight = getAvgTextPositionHeight(sortedWords);

        LineFactory lineFactory = LineFactory.init();

        var previousTextPosition = sortedWords.get(0);
        lineFactory.addToCurrentLine(previousTextPosition);
        for (Word currentTextPosition : sortedWords.subList(1, sortedWords.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<Word> words) {

        return words.stream().mapToDouble(Word::getHeight).average().orElseThrow();
    }


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

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


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

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


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

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


    @Getter
    @AllArgsConstructor
    private class LineFactory {

        List<Rectangle2D> lineBBox;

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

        List<List<List<Word>>> sequencesWithGapsByLines;
        List<List<Word>> sequencesWithGapsInCurrentLine;

        List<Word> currentSequencesWithoutGaps;

        List<List<Word>> sequencesByLines;
        List<Word> 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<Word>>> sequencesWithGapsByLines = new LinkedList<>();
            List<List<Word>> sequencesWithGapsInCurrentLine = new LinkedList<>();
            sequencesWithGapsByLines.add(sequencesWithGapsInCurrentLine);
            List<Word> currentSequencesWithoutGaps = new LinkedList<>();
            sequencesWithGapsInCurrentLine.add(currentSequencesWithoutGaps);

            List<List<Word>> sequencesByLines = new LinkedList<>();
            List<Word> 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<Word> words) {

            return RectangleTransformations.rectangle2DBBox(words.stream().map(Word::getBBox).toList());
        }


        public void addToCurrentLine(Word current) {

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


        public void addFinalLine() {

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

    }

}
