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

import static com.knecon.fforesight.service.layoutparser.processor.services.classification.ClassificationPatterns.ALPHANUMERIC;
import static com.knecon.fforesight.service.layoutparser.processor.services.classification.ClassificationPatterns.AMOUNT_PATTERN;
import static com.knecon.fforesight.service.layoutparser.processor.services.classification.ClassificationPatterns.AT_LEAST_3_CHARS_PATTERN;
import static com.knecon.fforesight.service.layoutparser.processor.services.classification.ClassificationPatterns.HEADLINE_PATTERN_WITH_SLASHES;
import static com.knecon.fforesight.service.layoutparser.processor.services.classification.ClassificationPatterns.HEADLINE_WITH_2_IDENTIFER_PATTERN;
import static com.knecon.fforesight.service.layoutparser.processor.services.classification.ClassificationPatterns.HEADLINE_WITH_SINGLE_IDENTIFER_PATTERN;
import static com.knecon.fforesight.service.layoutparser.processor.services.classification.ClassificationPatterns.TABLE_MID_SENTENCE_PATTERN;
import static com.knecon.fforesight.service.layoutparser.processor.services.classification.ClassificationPatterns.TABLE_OR_FIGURE_HEADLINE_PATTERN;

import java.util.ArrayList;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.regex.Matcher;

import org.springframework.stereotype.Service;

import com.knecon.fforesight.service.layoutparser.processor.model.AbstractPageBlock;
import com.knecon.fforesight.service.layoutparser.processor.model.ClassificationDocument;
import com.knecon.fforesight.service.layoutparser.processor.model.ClassificationPage;
import com.knecon.fforesight.service.layoutparser.processor.model.PageBlockType;
import com.knecon.fforesight.service.layoutparser.processor.model.text.AbstractBlockOnPage;
import com.knecon.fforesight.service.layoutparser.processor.model.text.ListIdentifier;
import com.knecon.fforesight.service.layoutparser.processor.model.text.TextPageBlock;
import com.knecon.fforesight.service.layoutparser.processor.utils.PositionUtils;

import lombok.AccessLevel;
import lombok.RequiredArgsConstructor;
import lombok.experimental.FieldDefaults;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@Service
@RequiredArgsConstructor
@FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE)
public class DocuMineClassificationService {

    public static final int SEPARATION_THRESHOLD = 10; // if the min distance between a textblock and all its surrounding blocks, the regexes can be more lenient.
    public static final int SURROUNDING_BLOCKS_RADIUS = 3; // number of surrounding blocks before and after the current textblock to be tested

    ListItemClassificationService listItemClassificationService;


    public void classifyDocument(ClassificationDocument document) {

        List<Double> headlineFontSizes = buildHeadlineFontSizes(document);
        List<AbstractBlockOnPage> blocks = buildBlocksPerPage(document);
        log.debug("Headline FontSizes are: {}", headlineFontSizes);

        HeadlineClassificationService headlineClassificationService = new HeadlineClassificationService();

        for (int i = 0; i < blocks.size(); i++) {
            AbstractBlockOnPage block = blocks.get(i);
            document.getLayoutDebugLayer().addTextBlockVisualizations(block.page().getTextBlocks(), block.page().getPageNumber());
            classifyBlock(headlineClassificationService, i, blocks, document, headlineFontSizes);
        }

    }


    private void classifyBlock(HeadlineClassificationService headlineClassificationService,
                               int currentIndex,
                               List<AbstractBlockOnPage> allBlocks,
                               ClassificationDocument document,
                               List<Double> headlineFontSizes) {

        TextPageBlock textBlock;
        if (allBlocks.get(currentIndex).block() instanceof TextPageBlock block) {
            textBlock = block;
        } else {
            return;
        }
        ClassificationPage page = allBlocks.get(currentIndex).page();
        List<AbstractPageBlock> surroundingBlocks = getSurroundingBlocksOnPage(currentIndex, allBlocks);

        log.debug("headlineFontSizes: {}", headlineFontSizes);
        var bodyTextFrame = page.getBodyTextFrame();

        Matcher headlineWith2IdentifierMatcher = HEADLINE_WITH_2_IDENTIFER_PATTERN.matcher(textBlock.toString());
        Matcher atLeast3Matcher = AT_LEAST_3_CHARS_PATTERN.matcher(textBlock.toString());
        Matcher headlineWithSlashesMatcher = HEADLINE_PATTERN_WITH_SLASHES.matcher(textBlock.toString());
        Matcher amountMatcher = AMOUNT_PATTERN.matcher(textBlock.toString());
        Matcher tableOrFigureMatcher = TABLE_OR_FIGURE_HEADLINE_PATTERN.matcher(textBlock.toString());
        Matcher tableMidSentenceMatcher = TABLE_MID_SENTENCE_PATTERN.matcher(textBlock.toString());
        Matcher headlineWithSingleIdentifierMatcher = HEADLINE_WITH_SINGLE_IDENTIFER_PATTERN.matcher(textBlock.toString());
        boolean isAtLeast3Characters = atLeast3Matcher.reset().find();
        boolean headlineWithSlashesMatches = headlineWithSlashesMatcher.reset().matches();
        boolean isAmount = amountMatcher.reset().find();
        int charCount = countChars(textBlock);

        boolean enoughChars = charCount > textBlock.getText().length() * 0.5;

        List<ListIdentifier> listIdentifiers = listItemClassificationService.findConfirmedListIdentifiers(currentIndex, allBlocks);
        document.getLayoutDebugLayer().addListIdentifiers(listIdentifiers);

        if (textBlock.getClassification() != null && textBlock.getClassification().isHeadline()) {
            headlineClassificationService.setLastHeadlineFromOutline(textBlock);
            return;
        }
        if (textBlock.getClassification() != null && (textBlock.getClassification().equals(PageBlockType.HEADER)
                                                      || textBlock.getClassification().equals(PageBlockType.FOOTER)
                                                      || textBlock.getClassification().equals(PageBlockType.TABLE_OF_CONTENTS_ITEM))) {
            return;
        }
        if (document.getFontSizeCounter().getMostPopular() == null) {
            textBlock.setClassification(PageBlockType.PARAGRAPH);
            return;
        }
        if (textBlock.getText().length() > 5
            && greaterOrEqualFontThanDocumentAverage(textBlock, document)
            && PositionUtils.getApproxLineCount(textBlock) < 5.9
            && ((textBlock.getMostPopularWordStyle().contains("bold") || textBlock.isUnderlined())//
                && Character.isDigit(textBlock.toString().charAt(0)) //
                && isAtLeast3Characters //
                && !textBlock.toString().contains(":") //
                || textBlock.toString().startsWith("APPENDIX") //
                || textBlock.toString().startsWith("FIGURE") //
                || textBlock.toString().startsWith("Continued TABLE") //
                || textBlock.toString().startsWith("TABLE"))
            && !textBlock.toString().endsWith(":")
            && isAtLeast3Characters
            && !isAmount
            && enoughChars) {

            setAsHeadline(headlineClassificationService, textBlock, document, headlineFontSizes);
        } else if (isAllCaps(textBlock)
                   && ALPHANUMERIC.matcher(Character.toString(textBlock.getText().charAt(0))).matches()
                   && hasSeparation(textBlock, surroundingBlocks)
                   && textBlock.getText().length() > 5
                   && isAtLeast3Characters
                   && !isAmount
                   && enoughChars
                   && !textBlock.toString().contains(":")
                   && !textBlock.toString().endsWith(".")
                   && PositionUtils.getApproxLineCount(textBlock) < 2.9) {

            setAsHeadline(headlineClassificationService, textBlock, document, headlineFontSizes);
        } else if (headlineWith2IdentifierMatcher.reset().find()
                   && PositionUtils.getApproxLineCount(textBlock) < 2.9
                   && isAtLeast3Characters
                   && !headlineWithSlashesMatches
                   && !isAmount) {

            setAsHeadline(headlineClassificationService, textBlock, document, headlineFontSizes);
        } else if (hasSeparation(textBlock, surroundingBlocks)//
                   && greaterOrEqualFontThanPageAverage(textBlock, page)//
                   && PositionUtils.getApproxLineCount(textBlock) < 2.9//
                   && (tableOrFigureMatcher.reset().find() || (headlineWithSingleIdentifierMatcher.reset().find() && listIdentifiers.isEmpty())) //
                   && tableMidSentenceMatcher.reset().results()
                              .count() <= 1 //
                   && !isAmount//
                   && !headlineWithSlashesMatches) {

            setAsHeadline(headlineClassificationService, textBlock, document, headlineFontSizes);
//        } else if (textBlock.getMostPopularWordFont().contains("bold")
//                   && greaterOrEqualFontThanPageAverage(textBlock, page)
//                   && textBlock.getWords().size() <= 6
//                   && PositionUtils.getApproxLineCount(textBlock) < 2.9
//                   && isAtLeast3Characters
//                   && charCount > textBlock.getText().length() * 0.75
//                   && !textBlock.getText().contains(":")
//                   && textBlock.getWidth() < page.getBodyTextFrame().getWidth() * 0.7) {
//
//            setAsHeadline(headlineClassificationService, textBlock, document, headlineFontSizes);
        } else if (!listIdentifiers.isEmpty()) {

            textBlock.setClassification(PageBlockType.LIST_ITEM);
        } else if (PositionUtils.isWithinBodyTextFrame(bodyTextFrame, textBlock)
                   && textBlock.getMostPopularWordFontSize() == document.getFontSizeCounter().getMostPopular()
                   && textBlock.getMostPopularWordStyle().equals("bold")
                   && !document.getFontStyleCounter().getMostPopular().equals("bold")) {

            textBlock.setClassification(PageBlockType.PARAGRAPH_BOLD);
        } else if (PositionUtils.isWithinBodyTextFrame(bodyTextFrame, textBlock)
                   && textBlock.getMostPopularWordFont().equals(document.getFontCounter().getMostPopular())
                   && textBlock.getMostPopularWordStyle().equals(document.getFontStyleCounter().getMostPopular())
                   && textBlock.getMostPopularWordFontSize() == document.getFontSizeCounter().getMostPopular()) {

            textBlock.setClassification(PageBlockType.PARAGRAPH);
        } else if (PositionUtils.isWithinBodyTextFrame(bodyTextFrame, textBlock)
                   && textBlock.getMostPopularWordFontSize() == document.getFontSizeCounter().getMostPopular()
                   && textBlock.getMostPopularWordStyle().equals("italic")
                   && !document.getFontStyleCounter().getMostPopular().equals("italic")
                   && PositionUtils.getApproxLineCount(textBlock) < 2.9) {

            textBlock.setClassification(PageBlockType.PARAGRAPH_ITALIC);
        } else {
            textBlock.setClassification(PageBlockType.PARAGRAPH);
        }
    }


    private int countChars(TextPageBlock textBlock) {

        int count = 0;

        for (int i = 0; i < textBlock.getText().length(); i++) {
            if (Character.isAlphabetic(textBlock.getText().charAt(i))) {
                count++;
            }
        }
        return count;
    }


    private static boolean greaterOrEqualFontThanPageAverage(TextPageBlock textBlock, ClassificationPage page) {

        return textBlock.getMostPopularWordHeight() >= page.getTextHeightCounter().getMostPopular() //
               || textBlock.getMostPopularWordFontSize() >= page.getFontSizeCounter().getMostPopular();
    }


    private static boolean greaterOrEqualFontThanDocumentAverage(TextPageBlock textBlock, ClassificationDocument document) {

        return textBlock.getMostPopularWordHeight() > document.getTextHeightCounter().getMostPopular() //
               || textBlock.getMostPopularWordFontSize() > document.getFontSizeCounter().getMostPopular();
    }


    private static boolean isAllCaps(TextPageBlock textBlock) {

        return textBlock.toString().equals(textBlock.toString().toUpperCase(Locale.ROOT));
    }


    private boolean hasSeparation(TextPageBlock textBlock, List<AbstractPageBlock> surroundingBlocks) {

        return surroundingBlocks.stream()
                .allMatch(surroundingBlock -> calculateSeparation(textBlock, surroundingBlock) > Math.pow(SEPARATION_THRESHOLD, 2));
    }


    private double calculateMinSeparation(TextPageBlock textBlock, List<AbstractPageBlock> surroundingBlocks) {

        return surroundingBlocks.stream()
                .mapToDouble(surroundingBlock -> calculateSeparation(textBlock, surroundingBlock))
                .min().orElse(Double.MAX_VALUE);
    }


    private static double calculateSeparation(TextPageBlock textBlock, AbstractPageBlock surroundingBlock) {

        return Math.pow(surroundingBlock.horizontalDistance(textBlock), 2) + Math.pow(surroundingBlock.verticalDistance(textBlock), 2);
    }


    private static void setAsHeadline(HeadlineClassificationService headlineClassificationService,
                                      TextPageBlock textBlock,
                                      ClassificationDocument document,
                                      List<Double> headlineFontSizes) {

        PageBlockType headlineType = HeadlineClassificationService.headlineClassByFontSize(textBlock, headlineFontSizes);
        headlineClassificationService.classifyHeadline(textBlock, headlineType);
        document.setHeadlines(true);
    }


    private List<AbstractBlockOnPage> buildBlocksPerPage(ClassificationDocument document) {

        List<AbstractBlockOnPage> blocks = new ArrayList<>();
        for (ClassificationPage page : document.getPages()) {
            for (AbstractPageBlock abstractPageBlock : page.getTextBlocks()) {
                if (abstractPageBlock instanceof TextPageBlock textBlock) {
                    if (textBlock.getClassification() != null && (textBlock.getClassification().equals(PageBlockType.HEADER) //
                                                                  || textBlock.getClassification().equals(PageBlockType.FOOTER))) {
                        continue;
                    }
                    blocks.add(new AbstractBlockOnPage(textBlock, page));
                }
            }
        }
        return blocks;
    }


    private static List<Double> buildHeadlineFontSizes(ClassificationDocument document) {

        if (document.getFontSizeCounter().getCountPerValue().size() <= 6) {
            return document.getFontSizeCounter().getValuesInReverseOrder();
        }

        List<Map.Entry<Double, Integer>> sortedEntries = new ArrayList<>(document.getFontSizeCounter().getCountPerValue().entrySet());
        sortedEntries.sort(Map.Entry.comparingByKey());

        int totalCount = sortedEntries.stream()
                .mapToInt(Map.Entry::getValue).sum();

        int cumulativeCount = 0;
        Iterator<Map.Entry<Double, Integer>> iterator = sortedEntries.iterator();
        while (iterator.hasNext()) {
            Map.Entry<Double, Integer> entry = iterator.next();
            cumulativeCount += entry.getValue();
            if (cumulativeCount > totalCount * 0.3) {
                break; // We've filtered the bottom 30%, so stop.
            }
            iterator.remove();
        }

        if (sortedEntries.size() < 6) {
            return document.getFontSizeCounter().getValuesInReverseOrder();
        }
        int clusterSize = Math.max(1, sortedEntries.size() / 6);

        List<List<Double>> clusters = new ArrayList<>();
        for (int i = 0; i < 6; i++) {
            clusters.add(new ArrayList<>());
        }

        for (int i = 0; i < sortedEntries.size(); i++) {
            int clusterIndex = Math.min(i / clusterSize, 5);
            clusters.get(clusterIndex).add(sortedEntries.get(i).getKey());
        }

        return clusters.stream()
                .map(cluster -> cluster.stream()
                        .mapToDouble(d -> d).average()
                        .orElseThrow())
                .sorted(Comparator.reverseOrder())
                .toList();
    }


    private List<AbstractPageBlock> getSurroundingBlocksOnPage(int originalIndex, List<AbstractBlockOnPage> textBlocks) {

        int start = Math.max(originalIndex - SURROUNDING_BLOCKS_RADIUS, 0);
        int end = Math.min(originalIndex + SURROUNDING_BLOCKS_RADIUS, textBlocks.size());
        List<AbstractPageBlock> surroundingBlocks = new ArrayList<>(2 * SURROUNDING_BLOCKS_RADIUS);
        for (int i = start; i < end; i++) {
            if (i == originalIndex) {
                continue;
            }
            if (textBlocks.get(i).block().getText().length() <= 1) {
                continue;
            }
            if (!textBlocks.get(i).page().equals(textBlocks.get(originalIndex).page())) {
                continue;
            }
            surroundingBlocks.add(textBlocks.get(i).block());
        }
        return surroundingBlocks;
    }

}



