package com.knecon.fforesight.service.layoutparser.processor.model.text;

import static java.util.stream.Collectors.toSet;

import java.awt.geom.Rectangle2D;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;

import com.fasterxml.jackson.annotation.JsonIgnore;
import com.knecon.fforesight.service.layoutparser.processor.model.AbstractPageBlock;
import com.knecon.fforesight.service.layoutparser.processor.model.FloatFrequencyCounter;
import com.knecon.fforesight.service.layoutparser.processor.model.PageBlockType;
import com.knecon.fforesight.service.layoutparser.processor.utils.DoubleComparisons;
import com.knecon.fforesight.service.layoutparser.processor.utils.RectangleTransformations;
import com.knecon.fforesight.service.layoutparser.processor.utils.TextNormalizationUtilities;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;

@EqualsAndHashCode(callSuper = true)
@Data
@AllArgsConstructor
@Builder
@NoArgsConstructor
public class TextPageBlock extends AbstractPageBlock {

    @Builder.Default
    private List<TextPositionSequence> sequences = new ArrayList<>();

    @JsonIgnore
    private int rotation;

    @JsonIgnore
    private String mostPopularWordFont;

    @JsonIgnore
    private String mostPopularWordStyle;

    @JsonIgnore
    private float mostPopularWordFontSize;

    @JsonIgnore
    private float mostPopularWordHeight;

    @JsonIgnore
    private float mostPopularWordSpaceWidth;

    @JsonIgnore
    private float highestFontSize;

    @JsonIgnore
    private PageBlockType classification;

    @JsonIgnore
    private boolean toDuplicate;


    @JsonIgnore
    public TextDirection getDir() {

        return sequences.get(0).getDir();
    }


    @JsonIgnore
    private float getPageHeight() {

        return sequences.get(0).getPageHeight();
    }


    @JsonIgnore
    private float getPageWidth() {

        return sequences.get(0).getPageWidth();
    }


    public static TextPageBlock merge(List<TextPageBlock> textBlocksToMerge) {

        List<TextPositionSequence> sequences = textBlocksToMerge.stream()
                .map(TextPageBlock::getSequences)
                .flatMap(java.util.Collection::stream)
                .toList();
        sequences = new ArrayList<>(sequences);
        return fromTextPositionSequences(sequences);
    }


    public static TextPageBlock fromTextPositionSequences(List<TextPositionSequence> wordBlockList) {

        TextPageBlock textBlock = null;

        FloatFrequencyCounter lineHeightFrequencyCounter = new FloatFrequencyCounter();
        FloatFrequencyCounter fontSizeFrequencyCounter = new FloatFrequencyCounter();
        FloatFrequencyCounter spaceFrequencyCounter = new FloatFrequencyCounter();
        StringFrequencyCounter fontFrequencyCounter = new StringFrequencyCounter();
        StringFrequencyCounter styleFrequencyCounter = new StringFrequencyCounter();

        for (TextPositionSequence wordBlock : wordBlockList) {

            lineHeightFrequencyCounter.add(wordBlock.getTextHeight());
            fontSizeFrequencyCounter.add(wordBlock.getFontSize());
            spaceFrequencyCounter.add(wordBlock.getSpaceWidth());
            fontFrequencyCounter.add(wordBlock.getFont());
            styleFrequencyCounter.add(wordBlock.getFontStyle());

            if (textBlock == null) {
                textBlock = new TextPageBlock(wordBlock.getMinXDirAdj(),
                                              wordBlock.getMaxXDirAdj(),
                                              wordBlock.getMinYDirAdj(),
                                              wordBlock.getMaxYDirAdj(),
                                              wordBlockList,
                                              wordBlock.getRotation());
            } else {
                TextPageBlock spatialEntity = textBlock.union(wordBlock);
                textBlock.resize(spatialEntity.getMinX(), spatialEntity.getMinY(), spatialEntity.getWidth(), spatialEntity.getHeight());
            }
        }

        if (textBlock != null) {
            textBlock.setMostPopularWordFont(fontFrequencyCounter.getMostPopular());
            textBlock.setMostPopularWordStyle(styleFrequencyCounter.getMostPopular());
            textBlock.setMostPopularWordFontSize(fontSizeFrequencyCounter.getMostPopular());
            textBlock.setMostPopularWordHeight(lineHeightFrequencyCounter.getMostPopular());
            textBlock.setMostPopularWordSpaceWidth(spaceFrequencyCounter.getMostPopular());
            textBlock.setHighestFontSize(fontSizeFrequencyCounter.getHighest());
        }

        if (textBlock != null
            && textBlock.getSequences() != null
            && textBlock.getSequences()
                       .stream()
                       .map(t -> DoubleComparisons.round(t.getMinYDirAdj(), 3))
                       .collect(toSet()).size() == 1) {
            textBlock.getSequences().sort(Comparator.comparing(TextPositionSequence::getMinXDirAdj));
        }
        return textBlock;
    }


    public Rectangle2D getBBox() {

        if (this.bBox == null) {
            this.bBox = sequences.stream()
                    .map(TextPositionSequence::getBoundingBox)
                    .collect(RectangleTransformations.collectBBox());
        }
        return this.bBox;
    }


    /**
     * Returns the minX value in pdf coordinate system.
     * Note: This needs to use Pdf Coordinate System where {0,0} rotated with the page rotation.
     * 0 -> LowerLeft
     * 90 -> UpperLeft
     * 180 -> UpperRight
     * 270 -> LowerRight
     *
     * @return the minX value in pdf coordinate system
     */
    @JsonIgnore
    public float getPdfMinX() {

        if (getDir().getDegrees() == 90) {
            return minY;
        } else if (getDir().getDegrees() == 180) {
            return getPageWidth() - maxX;

        } else if (getDir().getDegrees() == 270) {

            return getPageWidth() - maxY;
        } else {
            return minX;
        }
    }


    /**
     * Returns the maxX value in pdf coordinate system.
     * Note: This needs to use Pdf Coordinate System where {0,0} rotated with the page rotation.
     * 0 -> LowerLeft
     * 90 -> UpperLeft
     * 180 -> UpperRight
     * 270 -> LowerRight
     *
     * @return the maxX value in pdf coordinate system
     */
    @JsonIgnore
    public float getPdfMaxX() {

        if (getDir().getDegrees() == 90) {
            return maxY;
        } else if (getDir().getDegrees() == 180) {
            return getPageWidth() - minX;
        } else if (getDir().getDegrees() == 270) {
            return getPageWidth() - minY;

        } else {
            return maxX;
        }
    }


    /**
     * Returns the minY value in pdf coordinate system.
     * Note: This needs to use Pdf Coordinate System where {0,0} rotated with the page rotation.
     * 0 -> LowerLeft
     * 90 -> UpperLeft
     * 180 -> UpperRight
     * 270 -> LowerRight
     *
     * @return the minY value in pdf coordinate system
     */
    @JsonIgnore
    public float getPdfMinY() {

        if (getDir().getDegrees() == 90) {
            return minX;
        } else if (getDir().getDegrees() == 180) {
            return maxY;

        } else if (getDir().getDegrees() == 270) {
            return getPageHeight() - maxX;

        } else {
            return getPageHeight() - maxY;
        }
    }


    /**
     * Returns the maxY value in pdf coordinate system.
     * Note: This needs to use Pdf Coordinate System where {0,0} rotated with the page rotation.
     * 0 -> LowerLeft
     * 90 -> UpperLeft
     * 180 -> UpperRight
     * 270 -> LowerRight
     *
     * @return the maxY value in pdf coordinate system
     */
    @JsonIgnore
    public float getPdfMaxY() {

        if (getDir().getDegrees() == 90) {
            return maxX;
        } else if (getDir().getDegrees() == 180) {

            return minY;
        } else if (getDir().getDegrees() == 270) {
            return getPageHeight() - minX;
        } else {
            return getPageHeight() - minY;
        }
    }


    public TextPageBlock(float minX, float maxX, float minY, float maxY, List<TextPositionSequence> sequences, int rotation) {

        this.minX = minX;
        this.maxX = maxX;
        this.minY = minY;
        this.maxY = maxY;
        this.sequences = sequences;
        this.rotation = rotation;
    }


    public TextPageBlock union(TextPositionSequence r) {

        TextPageBlock union = this.copy();
        union.add(r);
        return union;
    }


    public TextPageBlock union(TextPageBlock r) {

        TextPageBlock union = this.copy();
        union.add(r);
        return union;
    }


    public void add(TextPageBlock r) {

        if (r.getMinX() < minX) {
            minX = r.getMinX();
        }
        if (r.getMaxX() > maxX) {
            maxX = r.getMaxX();
        }
        if (r.getMinY() < minY) {
            minY = r.getMinY();
        }
        if (r.getMaxY() > maxY) {
            maxY = r.getMaxY();
        }
        sequences.addAll(r.getSequences());
    }


    public void add(TextPositionSequence r) {

        if (r.getMinXDirAdj() < minX) {
            minX = r.getMinXDirAdj();
        }
        if (r.getMaxXDirAdj() > maxX) {
            maxX = r.getMaxXDirAdj();
        }
        if (r.getMinYDirAdj() < minY) {
            minY = r.getMinYDirAdj();
        }
        if (r.getMaxYDirAdj() > maxY) {
            maxY = r.getMaxYDirAdj();
        }
    }


    public TextPageBlock copy() {

        return new TextPageBlock(minX, maxX, minY, maxY, sequences, rotation);
    }


    public void resize(float x1, float y1, float width, float height) {

        set(x1, y1, x1 + width, y1 + height);
    }


    public void set(float x1, float y1, float x2, float y2) {

        this.minX = Math.min(x1, x2);
        this.maxX = Math.max(x1, x2);
        this.minY = Math.min(y1, y2);
        this.maxY = Math.max(y1, y2);
    }


    @Override
    public String toString() {

        StringBuilder builder = new StringBuilder();

        for (int i = 0; i < sequences.size(); i++) {
            String sequenceAsString = sequences.get(i).toString();
            // Fix for missing Whitespace. This is recognized in getSequences method. See PDFTextStripper Line 1730.
            if (i != 0 && sequences.get(i - 1).charAt(sequences.get(i - 1).length() - 1) != ' ' && sequenceAsString.charAt(0) != ' ') {
                builder.append(' ');
            }
            builder.append(sequenceAsString);
        }

        return builder.toString();

    }


    @Override
    @JsonIgnore
    public String getText() {

        StringBuilder sb = new StringBuilder();

        TextPositionSequence previous = null;
        for (TextPositionSequence word : sequences) {
            if (previous != null) {
                if (Math.abs(previous.getMaxYDirAdj() - word.getMaxYDirAdj()) > word.getTextHeight()) {
                    sb.append('\n');
                } else {
                    sb.append(' ');
                }
            }
            sb.append(word.toString());
            previous = word;
        }

        return TextNormalizationUtilities.removeHyphenLineBreaks(sb.toString());
    }


    public int getNumberOfLines() {

        int numberOfLines = 1;
        TextPositionSequence previous = null;
        for (TextPositionSequence word : sequences) {
            if (previous != null) {
                if (word.getMaxYDirAdj() - previous.getMaxYDirAdj() > word.getTextHeight()) {
                    numberOfLines++;
                }
            }
            previous = word;
        }
        return numberOfLines;
    }


    @Override
    public boolean isEmpty() {

        return sequences.isEmpty();
    }

}
