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

import static java.util.Collections.emptyList;

import java.util.Collection;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

import com.knecon.fforesight.service.layoutparser.internal.api.queue.LayoutParsingType;
import com.knecon.fforesight.service.layoutparser.processor.model.AbstractPageBlock;
import com.knecon.fforesight.service.layoutparser.processor.model.graph.nodes.Document;
import com.knecon.fforesight.service.layoutparser.processor.model.graph.nodes.GenericSemanticNode;
import com.knecon.fforesight.service.layoutparser.processor.model.graph.nodes.Page;
import com.knecon.fforesight.service.layoutparser.processor.model.graph.nodes.SemanticNode;
import com.knecon.fforesight.service.layoutparser.processor.model.graph.nodes.Table;
import com.knecon.fforesight.service.layoutparser.processor.model.graph.nodes.TableCell;
import com.knecon.fforesight.service.layoutparser.processor.model.graph.textblock.TextBlock;
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.TextPositionSequence;
import com.knecon.fforesight.service.layoutparser.processor.utils.TextPositionOperations;

import lombok.experimental.UtilityClass;

@UtilityClass
public class TableNodeFactory {

    public final double TABLE_CELL_MERGE_CONTENTS_SIZE_THRESHOLD = 0.05;


    public void addTable(LayoutParsingType layoutParsingType,
                         GenericSemanticNode parentNode,
                         List<TablePageBlock> tablesToMerge,
                         DocumentGraphFactory.Context context,
                         Document document) {

        setPageNumberInCells(tablesToMerge);
        Set<Page> pages = tablesToMerge.stream()
                .map(AbstractPageBlock::getPage)
                .map(context::getPage)
                .collect(Collectors.toSet());
        List<List<Cell>> mergedRows = tablesToMerge.stream()
                .map(TablePageBlock::getRows)
                .flatMap(Collection::stream)
                .toList();

        Table table = Table.builder().documentTree(context.getDocumentTree()).numberOfCols(mergedRows.isEmpty() ? 0 : mergedRows.get(0).size()).numberOfRows(mergedRows.size())
                .build();

        pages.forEach(page -> addTableToPage(page, parentNode, table));

        List<Integer> treeId = context.getDocumentTree().createNewChildEntryAndReturnId(parentNode, table);
        table.setTreeId(treeId);
        addTableCells(layoutParsingType, mergedRows, table, context, document);

        ifTableHasNoHeadersSetFirstRowAsHeaders(table);
    }


    private void setPageNumberInCells(List<TablePageBlock> tablesToMerge) {

        // For some reason I can't figure out, in some table cells, the ClassificationTextBlocks have 0 as page number
        // So I am fixing this here, but this should actually be fixed upstream.
        tablesToMerge.forEach(table -> table.getRows()
                .stream()
                .flatMap(Collection::stream)
                .peek(cell -> cell.setPageNumber(table.getPage()))
                .forEach(cell -> setPageNumberInTextBlocksWithPageNumberSetTo0(table, cell)));
    }


    private void setPageNumberInTextBlocksWithPageNumberSetTo0(TablePageBlock table, Cell cell) {

        cell.getTextBlocks()
                .stream()//
                .filter(tb -> tb.getPage() == 0)//
                .forEach(tb -> tb.setPage(table.getPage()));
    }


    @SuppressWarnings("PMD.UnusedPrivateMethod") // PMD actually flags this wrong
    private void addTableToPage(Page page, SemanticNode parentNode, Table table) {

        if (!page.getMainBody().contains(parentNode)) {
            parentNode.getPages().add(page);
        }

        page.getMainBody().add(table);
    }


    private void ifTableHasNoHeadersSetFirstRowAsHeaders(Table table) {

        if (table.streamHeaders()
                .findAny().isEmpty()) {
            table.streamRow(0)
                    .forEach(tableCellNode -> tableCellNode.setHeader(true));
        }
    }


    private void addTableCells(LayoutParsingType layoutParsingType, List<List<Cell>> rows, Table table, DocumentGraphFactory.Context context, Document document) {

        for (int rowIndex = 0; rowIndex < rows.size(); rowIndex++) {
            for (int colIndex = 0; colIndex < rows.get(rowIndex).size(); colIndex++) {
                addTableCell(layoutParsingType,
                             rows.get(rowIndex)
                                     .get(colIndex),
                             rowIndex,
                             colIndex,
                             table,
                             context,
                             document);
            }
        }
    }


    @SuppressWarnings("PMD.UnusedPrivateMethod") // PMD actually flags this wrong
    private void addTableCell(LayoutParsingType layoutParsingType,
                              Cell cell,
                              int rowIndex,
                              int colIndex,
                              Table tableNode,
                              DocumentGraphFactory.Context context,
                              Document document) {

        Page page = context.getPage(cell.getPageNumber());

        TableCell tableCell = TableCell.builder().documentTree(context.getDocumentTree()).row(rowIndex).col(colIndex).header(cell.isHeaderCell()).bBox(cell.getBounds2D())
                .build();
        page.getMainBody().add(tableCell);

        List<Integer> treeId = context.getDocumentTree().createNewTableChildEntryAndReturnId(tableNode, tableCell);
        tableCell.setTreeId(treeId);

        TextBlock textBlock;
        if (cell.getTextBlocks().isEmpty()) {
            tableCell.setLeafTextBlock(context.getTextBlockFactory().emptyTextBlock(tableNode, context, page));
        } else if (cell.getTextBlocks().size() == 1) {
            textBlock = context.getTextBlockFactory()
                    .buildAtomicTextBlock(cell.getTextBlocks()
                                                  .get(0).getSequences(), tableCell, context, page);
            tableCell.setLeafTextBlock(textBlock);
        } else if (firstTextBlockIsHeadline(cell)) {
            SectionNodeFactory.addSection(layoutParsingType,
                                          tableCell,
                                          cell.getTextBlocks()
                                                  .stream()
                                                  .map(tb -> (AbstractPageBlock) tb)
                                                  .toList(),
                                          emptyList(),
                                          context,
                                          document);
        } else if (cellAreaIsSmallerThanPageAreaTimesThreshold(cell, page)) {
            List<TextPositionSequence> sequences = TextPositionOperations.mergeAndSortTextPositionSequenceByYThenX(cell.getTextBlocks());
            textBlock = context.getTextBlockFactory().buildAtomicTextBlock(sequences, tableCell, context, page);
            tableCell.setLeafTextBlock(textBlock);
        } else {
            cell.getTextBlocks()
                    .forEach(tb -> DocumentGraphFactory.addParagraphOrHeadline(tableCell, tb, context, emptyList(), layoutParsingType));
        }
    }


    private boolean cellAreaIsSmallerThanPageAreaTimesThreshold(Cell cell, Page page) {

        return cell.getArea() < TABLE_CELL_MERGE_CONTENTS_SIZE_THRESHOLD * page.getHeight() * page.getWidth();
    }


    private boolean firstTextBlockIsHeadline(Cell cell) {

        return cell.getTextBlocks()
                .get(0).isHeadline();
    }

}
