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

import java.awt.geom.AffineTransform;
import java.awt.geom.Rectangle2D;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Objects;

import com.knecon.fforesight.service.layoutparser.processor.model.graph.Boundary;
import com.knecon.fforesight.service.layoutparser.processor.model.text.RedTextPosition;
import com.knecon.fforesight.service.layoutparser.processor.model.text.TextDirection;
import com.knecon.fforesight.service.layoutparser.processor.model.text.TextPositionSequence;

import lombok.experimental.UtilityClass;

@UtilityClass
public class SearchTextWithTextPositionFactory {

    public final int HEIGHT_PADDING = 2;
    // when checking for a hyphen linebreak, we need to check after a linebreak if the last hyphen was less than three symbols away.
    // We detect a linebreak as either a "\n" character or if two adjacent symbol's position differ in y-coordinates by at least one character height.
    // If there is a hyphen linebreak, the hyphen will be 1 position in front of a "\n" or 2 positions in front of the character which has a lower y-coordinate
    // This is why, we need to initialize this to < -2, otherwise, if the very first symbol is a \n we would detect a hyphen linebreak that isn't there.
    // Also, Integer.MIN_VALUE is a bad idea due to potential overflow during arithmetic operations. This is why the default should be -3.
    public final int MAX_HYPHEN_LINEBREAK_DISTANCE = 3;


    public SearchTextWithTextPositionDto buildSearchTextToTextPositionDto(List<TextPositionSequence> sequences) {

        if (sequences.isEmpty() || sequences.stream()
                .allMatch(sequence -> sequence.getTextPositions().isEmpty())) {
            return SearchTextWithTextPositionDto.empty();
        }

        Context context = new Context();

        RedTextPosition currentTextPosition = sequences.get(0).getTextPositions()
                .get(0);
        RedTextPosition previousTextPosition = RedTextPosition.builder().unicode(" ").directionAdjustedPosition(currentTextPosition.getDirectionAdjustedPosition()).build();

        for (TextPositionSequence word : sequences) {
            for (int i = 0; i < word.getTextPositions().size(); ++i) {

                currentTextPosition = word.getTextPositions()
                        .get(i);
                if (isLineBreak(currentTextPosition, previousTextPosition)) {
                    removeHyphenLinebreaks(context);
                    context.lineBreaksStringIdx.add(context.stringIdx);
                }
                if (!isRepeatedWhitespace(currentTextPosition.getUnicode(), previousTextPosition.getUnicode())) {
                    if (isHyphen(currentTextPosition.getUnicode())) {
                        context.lastHyphenIdx = context.stringIdx;
                    }
                    appendCurrentTextPosition(context, currentTextPosition);
                }

                previousTextPosition = currentTextPosition;
                ++context.positionIdx;
            }

            previousTextPosition = RedTextPosition.builder().unicode(" ").directionAdjustedPosition(previousTextPosition.getDirectionAdjustedPosition()).build();
            context.stringBuilder.append(" ");
            context.stringIdxToPositionIdx.add(context.positionIdx);
            ++context.stringIdx;
        }

        assert context.stringBuilder.length() == context.stringIdxToPositionIdx.size();

        List<Rectangle2D> positions = sequences.stream()
                .map(TextPositionSequence::getBoundingBox)
                .toList();

        return SearchTextWithTextPositionDto.builder()
                .searchText(context.stringBuilder.toString())
                .lineBreaks(context.lineBreaksStringIdx)
                .stringIdxToPositionIdx(context.stringIdxToPositionIdx)
                .positions(positions)
                .boldTextBoundaries(mergeToBoundaries(context.boldTextsStringIdx))
                .italicTextBoundaries(mergeToBoundaries(context.italicTextStringIdx))
                .build();
    }


    private void appendCurrentTextPosition(Context context, RedTextPosition currentTextPosition) {

        context.stringBuilder.append(currentTextPosition.getUnicode());

        // unicode characters with more than 16-bit encoding have a length > 1 in java strings
        for (int j = 0; j < currentTextPosition.getUnicode().length(); j++) {
            context.stringIdxToPositionIdx.add(context.positionIdx);
            addTextPositionWithFontType(currentTextPosition, "bold", context.boldTextsStringIdx, context.stringIdx);
            addTextPositionWithFontType(currentTextPosition, "italic", context.italicTextStringIdx, context.stringIdx);
        }
        context.stringIdx += currentTextPosition.getUnicode().length();
    }


    private void removeHyphenLinebreaks(Context context) {

        if (lastHyphenDirectlyBeforeLineBreak(context)) {
            context.stringBuilder.delete(context.lastHyphenIdx, context.stringBuilder.length());
            context.stringIdxToPositionIdx = context.stringIdxToPositionIdx.subList(0, context.lastHyphenIdx);
            context.stringIdx = context.lastHyphenIdx;
            context.lastHyphenIdx = -MAX_HYPHEN_LINEBREAK_DISTANCE;
        }
    }


    private boolean lastHyphenDirectlyBeforeLineBreak(Context context) {

        return context.stringIdx - context.lastHyphenIdx < MAX_HYPHEN_LINEBREAK_DISTANCE;
    }


    private static List<Boundary> mergeToBoundaries(List<Integer> integers) {

        if (integers.isEmpty()) {
            return Collections.emptyList();
        }
        List<Boundary> boundaries = new LinkedList<>();
        int start = integers.get(0);
        int end = integers.get(0) + 1;
        for (int current : integers) {
            if (current > end + 1) {
                boundaries.add(new Boundary(start, end));
                start = current;
            }
            end = current + 1;
        }
        if (boundaries.isEmpty()) {
            boundaries.add(new Boundary(start, end));
        }
        return boundaries;
    }


    private static void addTextPositionWithFontType(RedTextPosition currentTextPosition, String fontType, List<Integer> fontTypePositions, int stringIdx) {

        if (currentTextPosition.getFontName() != null && currentTextPosition.getFontName().toLowerCase(Locale.ROOT).contains(fontType)) {
            fontTypePositions.add(stringIdx);
        }
    }


    private boolean isLineBreak(RedTextPosition currentTextPosition, RedTextPosition previousTextPosition) {

        return Objects.equals(currentTextPosition.getUnicode(), "\n") || isDeltaYLargerThanTextHeight(currentTextPosition, previousTextPosition);
    }


    private boolean isDeltaYLargerThanTextHeight(RedTextPosition currentPosition, RedTextPosition previousPosition) {

        if (previousPosition == null) {
            return false;
        }

        double deltaY = Math.abs(currentPosition.getYDirAdj() - previousPosition.getYDirAdj());
        return deltaY >= currentPosition.getHeightDir();
    }


    private boolean isRepeatedWhitespace(String currentUnicode, String previousUnicode) {

        return Objects.equals(previousUnicode, " ") && Objects.equals(currentUnicode, " ");
    }


    private boolean isHyphen(String unicodeCharacter) {

        return Objects.equals(unicodeCharacter, "-") || //
               Objects.equals(unicodeCharacter, "~") || //
               Objects.equals(unicodeCharacter, "‐") || //
               Objects.equals(unicodeCharacter, "‒") || //
               Objects.equals(unicodeCharacter, "⁻") || //
               Objects.equals(unicodeCharacter, "−") || //
               Objects.equals(unicodeCharacter, "﹣") || //
               Objects.equals(unicodeCharacter, "゠") || //
               Objects.equals(unicodeCharacter, "⁓") || //
               Objects.equals(unicodeCharacter, "‑") || //
               Objects.equals(unicodeCharacter, "\u00AD");
    }


    public Rectangle2D mapRedTextPositionToInitialUserSpace(RedTextPosition textPosition, TextPositionSequence sequence) {

        float textHeight = sequence.getTextHeight() + HEIGHT_PADDING;
        Rectangle2D rectangle2D = new Rectangle2D.Double(textPosition.getXDirAdj(),
                textPosition.getYDirAdj() - textHeight,
                textPosition.getWidthDirAdj(),
                textHeight + HEIGHT_PADDING);

        AffineTransform transform = new AffineTransform();

        if (sequence.getDir() == TextDirection.ZERO || sequence.getDir() == TextDirection.HALF_CIRCLE) {
            transform.rotate(sequence.getDir().getRadians(), sequence.getPageWidth() / 2f, sequence.getPageHeight() / 2f);
            transform.translate(0f, sequence.getPageHeight());
        } else if (sequence.getDir() == TextDirection.QUARTER_CIRCLE) {
            transform.rotate(sequence.getDir().getRadians(), sequence.getPageWidth() / 2f, sequence.getPageWidth() / 2f);
            transform.translate(0f, sequence.getPageWidth());
        } else {
            transform.rotate(sequence.getDir().getRadians(), sequence.getPageHeight() / 2f, sequence.getPageHeight() / 2f);
            transform.translate(0f, sequence.getPageWidth());
        }
        transform.scale(1., -1.);

        return transform.createTransformedShape(rectangle2D).getBounds2D();
    }


    private class Context {

        List<Integer> stringIdxToPositionIdx = new LinkedList<>();
        List<Integer> lineBreaksStringIdx = new LinkedList<>();
        List<Integer> boldTextsStringIdx = new LinkedList<>();
        List<Integer> italicTextStringIdx = new LinkedList<>();
        StringBuilder stringBuilder = new StringBuilder();

        int stringIdx;
        int positionIdx;

        int lastHyphenIdx = -MAX_HYPHEN_LINEBREAK_DISTANCE;

    }

}
