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

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

import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.ListIterator;

import org.springframework.stereotype.Service;

import com.knecon.fforesight.service.layoutparser.processor.docstrum.DocstrumSegmentationService;
import com.knecon.fforesight.service.layoutparser.processor.docstrum.model.Zone;
import com.knecon.fforesight.service.layoutparser.processor.docstrum.utils.DoubleUtils;
import com.knecon.fforesight.service.layoutparser.processor.model.AbstractPageBlock;
import com.knecon.fforesight.service.layoutparser.processor.model.ClassificationPage;
import com.knecon.fforesight.service.layoutparser.processor.model.FloatFrequencyCounter;
import com.knecon.fforesight.service.layoutparser.processor.model.table.CleanRulings;
import com.knecon.fforesight.service.layoutparser.processor.model.table.Ruling;
import com.knecon.fforesight.service.layoutparser.processor.model.table.TablePageBlock;
import com.knecon.fforesight.service.layoutparser.processor.model.text.StringFrequencyCounter;
import com.knecon.fforesight.service.layoutparser.processor.model.text.TextPageBlock;
import com.knecon.fforesight.service.layoutparser.processor.model.text.TextPositionSequence;
import com.knecon.fforesight.service.layoutparser.processor.utils.RulingTextDirAdjustUtil;
import com.knecon.fforesight.service.layoutparser.processor.visualization.LayoutparsingVisualizations;

import lombok.RequiredArgsConstructor;

@SuppressWarnings("all")
@Service
@RequiredArgsConstructor
public class DocstrumBlockificationService {

    private final DocstrumSegmentationService docstrumSegmentationService;

    static final float THRESHOLD = 1f;


    public ClassificationPage blockify(List<TextPositionSequence> textPositions, CleanRulings rulings, boolean xyOrder, LayoutparsingVisualizations visualizations) {

        CleanRulings usedRulings = rulings.withoutTextRulings();

        var zones = docstrumSegmentationService.segmentPage(textPositions, xyOrder, usedRulings, visualizations);

        if (!textPositions.isEmpty()) {
            visualizations.addZoneVisualizations(zones, textPositions.get(0).getPage());
            visualizations.addLineVisualizationsFromZones(zones, textPositions.get(0).getPage());
            visualizations.addCharactersWithNeighbours(zones, textPositions.get(0).getPage());
        }

        var pageBlocks = toAbstractPageBlocks(zones, usedRulings.getHorizontals(), usedRulings.getVerticals(), xyOrder, usedRulings);

        if (xyOrder) {
            sortPageBlocksXThenY(pageBlocks);
        }

        var classificationPage = new ClassificationPage(pageBlocks);
        classificationPage.setCleanRulings(rulings);

        mergeIntersectingBlocks(classificationPage, 0, 0);

        return classificationPage;
    }


    private static void sortPageBlocksXThenY(List<AbstractPageBlock> pageBlocks) {

        pageBlocks.sort(Comparator.comparing(AbstractPageBlock::getMinY, (o1, o2) -> DoubleUtils.compareDouble(o1, o2, THRESHOLD))
                                .thenComparing(AbstractPageBlock::getMinX, (o1, o2) -> DoubleUtils.compareDouble(o1, o2, THRESHOLD)));
        pageBlocks.sort(new Comparator<AbstractPageBlock>() {
            @Override
            public int compare(AbstractPageBlock o1, AbstractPageBlock o2) {

                return Math.abs(o1.getMinY() - o2.getMinY()) < 5 && o1.getMinX() < o2.getMinX() == true ? -1 : 0;
            }
        });
    }


    private List<AbstractPageBlock> toAbstractPageBlocks(List<Zone> zones,
                                                         List<Ruling> horizontalRulings,
                                                         List<Ruling> verticalRulings,
                                                         boolean xyOrder,
                                                         CleanRulings usedRulings) {

        List<AbstractPageBlock> abstractPageBlocks = new ArrayList<>();
        zones.forEach(zone -> {

            List<TextPositionSequence> textPositionSequences = new ArrayList<>();
            zone.getLines()
                    .forEach(line -> {
                        line.getWords()
                                .forEach(word -> {
                                    textPositionSequences.add(new TextPositionSequence(word.getTextPositions(), word.getPage()));
                                });
                    });

            abstractPageBlocks.add(buildTextBlock(textPositionSequences, 0));
        });

        return abstractPageBlocks;
    }


    public void combineBlocks(ClassificationPage page) {

        TextPageBlock previous = new TextPageBlock();
        ListIterator<AbstractPageBlock> itty = page.getTextBlocks().listIterator();
        while (itty.hasNext()) {

            AbstractPageBlock block = itty.next();
            if (block instanceof TablePageBlock) {
                previous = new TextPageBlock();
                continue;
            }
            TextPageBlock current = (TextPageBlock) block;

            if (previous != null && !previous.getSequences().isEmpty()) {

                if (current.getDir() != previous.getDir()) {
                    previous = current;
                    continue;
                }

                if (areTheOnlyTwoBlocksOnHeightsWithBothMoreThanTwoLines(previous, current, page)) {
                    previous = combineBlocksAndResetIterator(previous, current, itty, true);
                    continue;
                }

                if (previous.almostIntersects(current, 0, 0)) {
                    previous = combineBlocksAndResetIterator(previous, current, itty, previous.isToDuplicate());
                    continue;
                }

                if (isSameTopOrBottomWithPreviousHavingMaxTwoLinesAndCurrentThanOneAndMax4OtherBlocksOnHeight(previous, current, page)) {
                    previous = combineBlocksAndResetIterator(previous, current, itty, previous.isToDuplicate());
                    continue;
                }

                if (isOnlyIntersectingYAndOnelineOrPrevoiusTwoLines(previous, current, page)) {
                    previous = combineBlocksAndResetIterator(previous, current, itty, previous.isToDuplicate());
                    continue;
                }

            }
            previous = current;
        }

        mergeIntersectingBlocks(page, 0, 6.5f);
    }


    private boolean isOnlyIntersectingYAndOnelineOrPrevoiusTwoLines(TextPageBlock previous, TextPageBlock current, ClassificationPage page) {

        return current.intersectsY(previous) //
               && (previous.getNumberOfLines() == 1 && current.getNumberOfLines() >= 1 || previous.getNumberOfLines() == 2 && current.getNumberOfLines() == 1) //
               && numberOfYIntersectionsOfSmallerBlocksWithOtherBlocks(current, previous, page.getTextBlocks()) <= 0;
    }


    private boolean isSameTopOrBottomWithPreviousHavingMaxTwoLinesAndCurrentThanOneAndMax4OtherBlocksOnHeight(TextPageBlock previous,
                                                                                                              TextPageBlock current,
                                                                                                              ClassificationPage page) {

        return (Math.abs(previous.getMaxY() - current.getMaxY()) < THRESHOLD || Math.abs(previous.getMinY() - current.getMinY()) < THRESHOLD) //
               && (previous.getNumberOfLines() == 1 && current.getNumberOfLines() >= 1 || previous.getNumberOfLines() >= 2 && current.getNumberOfLines() == 1) //
               && !hasBetween(current, previous, page.getTextBlocks()) && numberOfYIntersectionsOfSmallerBlocksWithOtherBlocks(current, previous, page.getTextBlocks()) <= 4;
    }


    private boolean areTheOnlyTwoBlocksOnHeightsWithBothMoreThanTwoLines(TextPageBlock previous, TextPageBlock current, ClassificationPage page) {

        return previous.getNumberOfLines() >= 2 && current.getNumberOfLines() >= 2 //
               && previous.intersectsY(current) //
               && numberOfYIntersectionsOfSmallerBlocksWithOtherBlocks(current, previous, page.getTextBlocks()) == 0;
    }


    private TextPageBlock combineBlocksAndResetIterator(TextPageBlock previous, TextPageBlock current, ListIterator<AbstractPageBlock> itty, boolean toDuplicate) {

        previous.getSequences().addAll(current.getSequences());
        previous = buildTextBlock(previous.getSequences(), 0);
        previous.setToDuplicate(toDuplicate);
        itty.remove();
        itty.previous();
        itty.set(previous);
        itty.next();
        return previous;
    }


    private boolean hasBetween(TextPageBlock block, TextPageBlock other, List<AbstractPageBlock> allBlocks) {

        for (AbstractPageBlock current : allBlocks) {

            if (current == other || current == block) {
                continue;
            }

            if (other.intersectsY(current) && other.getMaxX() <= current.getMinX() && current.getMaxX() <= block.getMinX()) {
                return true;
            }
        }

        return false;
    }


    private int numberOfYIntersectionsOfSmallerBlocksWithOtherBlocks(TextPageBlock block, TextPageBlock other, List<AbstractPageBlock> allBlocks) {

        double minY = Math.min(block.getMinY(), other.getMinY());
        double maxY = Math.min(block.getMaxY(), other.getMaxY());

        int numberOfYIntersections = 0;
        for (AbstractPageBlock current : allBlocks) {

            if (current == other || current == block) {
                continue;
            }

            if (minY <= current.getMaxY() && maxY >= current.getMinY()) {
                numberOfYIntersections++;
            }
        }

        return numberOfYIntersections;
    }


    public void mergeIntersectingBlocks(ClassificationPage page, float xThreshold, float yThreshold) {

        var blocks = page.getTextBlocks();
        ListIterator<AbstractPageBlock> itty = blocks.listIterator();
        while (itty.hasNext()) {
            AbstractPageBlock block = itty.next();
            if (block == null) {
                continue;
            }
            if (block instanceof TablePageBlock) {
                continue;
            }

            TextPageBlock current = (TextPageBlock) block;

            for (int i = 0; i < blocks.size(); i++) {

                if (blocks.get(i) == null) {
                    continue;
                }
                if (blocks.get(i) == current) {
                    continue;
                }
                if (blocks.get(i) instanceof TablePageBlock) {
                    continue;
                }

                TextPageBlock inner = (TextPageBlock) blocks.get(i);

                if (page.getCleanRulings().lineBetween(inner.getBBox(), current.getBBox())) {
                    continue;
                }

                if (current.getDir() == inner.getDir() && current.almostIntersects(inner, yThreshold, xThreshold)) {

                    boolean toDuplicate = current.isToDuplicate() || inner.isToDuplicate();
                    current.getSequences().addAll(inner.getSequences());
                    current = buildTextBlock(current.getSequences(), 0);

                    current.setToDuplicate(toDuplicate);
                    blocks.set(i, null);
                    itty.set(current);
                }
            }
        }
        var blocksIterator = blocks.iterator();
        while (blocksIterator.hasNext()) {
            if (blocksIterator.next() == null) {
                blocksIterator.remove();
            }
        }
    }


    public List<AbstractPageBlock> splitZonesAtRulings(List<TextPositionSequence> textPositions, List<Ruling> horizontalRulingLines, List<Ruling> verticalRulingLines) {

        int indexOnPage = 0;
        List<TextPositionSequence> chunkWords = new ArrayList<>();
        List<AbstractPageBlock> chunkBlockList = new ArrayList<>();

        float minX = 1000, maxX = 0, minY = 1000, maxY = 0;
        TextPositionSequence prev = null;

        for (TextPositionSequence word : textPositions) {

            boolean isSplitByRuling = isSplitByRuling(minX, minY, maxX, maxY, word, horizontalRulingLines, verticalRulingLines);
            boolean splitByDir = prev != null && !prev.getDir().equals(word.getDir());

            if (prev != null && (splitByDir || isSplitByRuling)) {

                TextPageBlock cb1 = buildTextBlock(chunkWords, indexOnPage);
                indexOnPage++;

                chunkBlockList.add(cb1);
                chunkWords = new ArrayList<>();

                minX = 1000;
                maxX = 0;
                minY = 1000;
                maxY = 0;
                prev = null;
            }

            chunkWords.add(word);

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

        TextPageBlock cb1 = buildTextBlock(chunkWords, indexOnPage);
        if (cb1 != null) {
            chunkBlockList.add(cb1);
        }

        return chunkBlockList;
    }


    private boolean equalsWithThreshold(float f1, float f2) {

        return Math.abs(f1 - f2) < THRESHOLD;
    }


    private TextPageBlock buildTextBlock(List<TextPositionSequence> wordBlockList, int indexOnPage) {

        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 -> round(t.getMinYDirAdj(), 3))
                       .collect(toSet()).size() == 1) {
            textBlock.getSequences().sort(Comparator.comparing(TextPositionSequence::getMinXDirAdj));
        }
        return textBlock;
    }


    private boolean isSplitByRuling(float minX,
                                    float minY,
                                    float maxX,
                                    float maxY,
                                    TextPositionSequence word,
                                    List<Ruling> horizontalRulingLines,
                                    List<Ruling> verticalRulingLines) {

        return isSplitByRuling(maxX, minY, word.getMinXDirAdj(), word.getMinYDirAdj(), verticalRulingLines, word.getDir().getDegrees(), word.getPageWidth(), word.getPageHeight())
               //
               || isSplitByRuling(minX,
                                  minY,
                                  word.getMinXDirAdj(),
                                  word.getMaxYDirAdj(),
                                  horizontalRulingLines,
                                  word.getDir().getDegrees(),
                                  word.getPageWidth(),
                                  word.getPageHeight())
               //
               || isSplitByRuling(maxX,
                                  minY,
                                  word.getMinXDirAdj(),
                                  word.getMinYDirAdj(),
                                  horizontalRulingLines,
                                  word.getDir().getDegrees(),
                                  word.getPageWidth(),
                                  word.getPageHeight())
               //
               || isSplitByRuling(minX,
                                  minY,
                                  word.getMinXDirAdj(),
                                  word.getMaxYDirAdj(),
                                  verticalRulingLines,
                                  word.getDir().getDegrees(),
                                  word.getPageWidth(),
                                  word.getPageHeight());
    }


    private boolean isSplitByRuling(float previousX2, float previousY1, float currentX1, float currentY1, List<Ruling> rulingLines, float dir, float pageWidth, float pageHeight) {

        for (Ruling ruling : rulingLines) {
            var line = RulingTextDirAdjustUtil.convertToDirAdj(ruling, dir, pageWidth, pageHeight);
            if (line.intersectsLine(previousX2, previousY1, currentX1, currentY1)) {
                return true;
            }
        }
        return false;
    }


    private double round(float value, int decimalPoints) {

        var d = Math.pow(10, decimalPoints);
        return Math.round(value * d) / d;
    }

}
