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

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

import org.apache.logging.log4j.util.Strings;
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.ClassificationFooter;
import com.knecon.fforesight.service.layoutparser.processor.model.ClassificationHeader;
import com.knecon.fforesight.service.layoutparser.processor.model.ClassificationPage;
import com.knecon.fforesight.service.layoutparser.processor.model.ClassificationSection;
import com.knecon.fforesight.service.layoutparser.processor.model.PageBlockType;
import com.knecon.fforesight.service.layoutparser.processor.model.image.ClassifiedImage;
import com.knecon.fforesight.service.layoutparser.processor.model.table.Cell;
import com.knecon.fforesight.service.layoutparser.processor.model.table.TablePageBlock;
import com.knecon.fforesight.service.layoutparser.processor.model.text.TextPageBlock;
import com.knecon.fforesight.service.layoutparser.processor.model.text.UnclassifiedText;

import lombok.extern.slf4j.Slf4j;

@Slf4j
@Service
public class SectionsBuilderService {

    public void buildSections(ClassificationDocument document) {

        List<AbstractPageBlock> chunkWords = new ArrayList<>();
        List<ClassificationSection> chunkBlockList = new ArrayList<>();
        List<ClassificationHeader> headers = new ArrayList<>();
        List<ClassificationFooter> footers = new ArrayList<>();
        List<UnclassifiedText> unclassifiedTexts = new ArrayList<>();

        AbstractPageBlock prev = null;

        String lastHeadline = "";
        TablePageBlock previousTable = null;
        for (ClassificationPage page : document.getPages()) {
            List<TextPageBlock> header = new ArrayList<>();
            List<TextPageBlock> footer = new ArrayList<>();
            for (AbstractPageBlock current : page.getTextBlocks()) {

                if (current.getClassification() == null) {
                    continue;
                }

                current.setPage(page.getPageNumber());

                if (current.getClassification().equals(PageBlockType.HEADER)) {
                    header.add((TextPageBlock) current);
                    continue;
                }

                if (current.getClassification().equals(PageBlockType.FOOTER)) {
                    footer.add((TextPageBlock) current);
                    continue;
                }

                if (prev != null && current.getClassification().isHeadline() && !prev.getClassification().isHeadline() || !document.isHeadlines()) {
                    ClassificationSection chunkBlock = buildTextBlock(chunkWords, lastHeadline);
                    chunkBlock.setHeadline(lastHeadline);
                    if (document.isHeadlines()) {
                        lastHeadline = current.getText();
                    }
                    chunkBlockList.add(chunkBlock);
                    chunkWords = new ArrayList<>();
                    if (!chunkBlock.getTables().isEmpty()) {
                        previousTable = chunkBlock.getTables().get(chunkBlock.getTables().size() - 1);
                    }
                }
                if (current instanceof TablePageBlock table) {
                    // Distribute header information for subsequent tables
                    mergeTableMetadata(table, previousTable);
                    previousTable = table;
                }
                chunkWords.add(current);
                prev = current;
            }

            if (!header.isEmpty()) {
                headers.add(new ClassificationHeader(header));
            }
            if (!footer.isEmpty()) {
                footers.add(new ClassificationFooter(footer));
            }
        }

        ClassificationSection chunkBlock = buildTextBlock(chunkWords, lastHeadline);
        chunkBlock.setHeadline(lastHeadline);
        chunkBlockList.add(chunkBlock);

        document.setSections(chunkBlockList);
        document.setHeaders(headers);
        document.setFooters(footers);
        document.setUnclassifiedTexts(unclassifiedTexts);
    }


    public void buildParagraphDebugSections(ClassificationDocument document) {

        List<ClassificationSection> sections = new ArrayList<>();
        for (var page : document.getPages()) {
            page.getTextBlocks().forEach(block -> {
                block.setPage(page.getPageNumber());
                var section = buildTextBlock(List.of(block), Strings.EMPTY);
                sections.add(section);
            });
        }
        document.setSections(sections);
    }


    public void addImagesToSections(ClassificationDocument document) {

        Map<Integer, List<ClassificationSection>> sectionMap = new HashMap<>();
        for (ClassificationSection section : document.getSections()) {
            for (AbstractPageBlock container : section.getPageBlocks()) {

                List<ClassificationSection> sectionsOnPage = sectionMap.computeIfAbsent(container.getPage(), c -> new ArrayList<>());
                if (sectionsOnPage.contains(section)) {
                    continue;
                }
                sectionsOnPage.add(section);
            }
        }

        if (sectionMap.isEmpty()) {
            ClassificationSection section = new ClassificationSection();
            document.getSections().add(section);
            sectionMap.computeIfAbsent(1, x -> new ArrayList<>()).add(section);
        }

        // first page is always a paragraph, else we can't process pages 1..N,
        // where N is the first found page with a paragraph
        if (sectionMap.get(1) == null) {
            ClassificationSection section = new ClassificationSection();
            document.getSections().add(section);
            sectionMap.computeIfAbsent(1, x -> new ArrayList<>()).add(section);
        }

        for (ClassificationPage page : document.getPages()) {
            for (ClassifiedImage image : page.getImages()) {
                List<ClassificationSection> sectionsOnPage = sectionMap.get(page.getPageNumber());
                if (sectionsOnPage == null) {
                    int i = page.getPageNumber();
                    while (sectionsOnPage == null) {
                        sectionsOnPage = sectionMap.get(i);
                        i--;
                    }
                }
                for (ClassificationSection section : sectionsOnPage) {
                    Float xMin = null;
                    Float yMin = null;
                    Float xMax = null;
                    Float yMax = null;

                    for (AbstractPageBlock abs : section.getPageBlocks()) {
                        if (abs.getPage() != page.getPageNumber()) {
                            continue;
                        }

                        if (abs.getMinX() < abs.getMaxX()) {
                            if (xMin == null || abs.getMinX() < xMin) {
                                xMin = abs.getMinX();
                            }
                            if (xMax == null || abs.getMaxX() > xMax) {
                                xMax = abs.getMaxX();
                            }
                        } else {
                            if (xMin == null || abs.getMaxX() < xMin) {
                                xMin = abs.getMaxX();
                            }
                            if (xMax == null || abs.getMinX() > xMax) {
                                xMax = abs.getMinX();
                            }
                        }

                        if (abs.getMinY() < abs.getMaxY()) {
                            if (yMin == null || abs.getMinY() < yMin) {
                                yMin = abs.getMinY();
                            }
                            if (yMax == null || abs.getMaxY() > yMax) {
                                yMax = abs.getMaxY();
                            }
                        } else {
                            if (yMin == null || abs.getMaxY() < yMin) {
                                yMin = abs.getMaxY();
                            }
                            if (yMax == null || abs.getMinY() > yMax) {
                                yMax = abs.getMinY();
                            }
                        }

                    }

                    log.debug("Image position x: {}, y: {}", image.getPosition().getX(), image.getPosition().getY());
                    log.debug("Paragraph position xMin: {}, xMax: {}, yMin: {}, yMax: {}", xMin, xMax, yMin, yMax);

                    if (xMin != null && xMax != null && yMin != null && yMax != null && image.getPosition().getX() >= xMin && image.getPosition()
                            .getX() <= xMax && image.getPosition().getY() >= yMin && image.getPosition().getY() <= yMax) {
                        section.getImages().add(image);
                        image.setAppendedToSection(true);
                        break;
                    }
                }
                if (!image.isAppendedToSection()) {
                    log.debug("Image uses first paragraph");
                    sectionsOnPage.get(0).getImages().add(image);
                    image.setAppendedToSection(true);
                }
            }
        }
    }


    private void mergeTableMetadata(TablePageBlock currentTable, TablePageBlock previousTable) {

        // Distribute header information for subsequent tables
        if (previousTable != null && hasInvalidHeaderInformation(currentTable) && hasValidHeaderInformation(previousTable)) {
            List<Cell> previousTableNonHeaderRow = getRowWithNonHeaderCells(previousTable);
            List<Cell> tableNonHeaderRow = getRowWithNonHeaderCells(currentTable);
            // Allow merging of tables if header row is separated from first logical non-header row
            if (previousTableNonHeaderRow.isEmpty() && previousTable.getRowCount() == 1 && previousTable.getRows().get(0).size() == tableNonHeaderRow.size()) {
                previousTableNonHeaderRow = previousTable.getRows().get(0).stream().map(cell -> {
                    Cell fakeCell = new Cell(cell.getPoints()[0], cell.getPoints()[2]);
                    fakeCell.setHeaderCells(Collections.singletonList(cell));
                    return fakeCell;
                }).collect(Collectors.toList());
            }
            if (previousTableNonHeaderRow.size() == tableNonHeaderRow.size()) {
                for (int i = currentTable.getRowCount() - 1; i >= 0; i--) { // Non header rows are most likely at bottom of table
                    List<Cell> row = currentTable.getRows().get(i);
                    if (row.size() == tableNonHeaderRow.size() && row.stream().allMatch(cell -> cell.getHeaderCells().isEmpty())) {
                        for (int j = 0; j < row.size(); j++) {
                            row.get(j).setHeaderCells(previousTableNonHeaderRow.get(j).getHeaderCells());
                        }
                    }
                }
            }
        }
    }


    private ClassificationSection buildTextBlock(List<AbstractPageBlock> wordBlockList, String lastHeadline) {

        ClassificationSection section = new ClassificationSection();

        for (AbstractPageBlock container : wordBlockList) {
            if (container instanceof TablePageBlock table) {

                if (lastHeadline == null || lastHeadline.isEmpty()) {
                    table.setHeadline("Text in table");
                } else {
                    table.setHeadline("TablePageBlock in: " + lastHeadline);
                }

                section.getPageBlocks().add(table);
                continue;
            }

            TextPageBlock wordBlock = (TextPageBlock) container;
            section.getPageBlocks().add(wordBlock);
        }
        return section;
    }


    private boolean hasValidHeaderInformation(TablePageBlock table) {

        return !hasInvalidHeaderInformation(table);
    }


    private boolean hasInvalidHeaderInformation(TablePageBlock table) {

        return table.getRows().stream().flatMap(row -> row.stream().filter(cell -> !cell.getHeaderCells().isEmpty())).findAny().isEmpty();

    }


    private List<Cell> getRowWithNonHeaderCells(TablePageBlock table) {

        for (int i = table.getRowCount() - 1; i >= 0; i--) { // Non header rows are most likely at bottom of table
            List<Cell> row = table.getRows().get(i);
            if (row.size() == 1) {
                continue;
            }
            boolean allNonHeader = true;
            for (Cell cell : row) {
                if (cell.isHeaderCell()) {
                    allNonHeader = false;
                    break;
                }
            }
            if (allNonHeader) {
                return row;
            }
        }

        return Collections.emptyList();

    }

}
