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

import static com.knecon.fforesight.service.layoutparser.processor.utils.GeometricComparators.CELL_SIZE_COMPARATOR;
import static com.knecon.fforesight.service.layoutparser.processor.utils.GeometricComparators.RECTANGLE_SIZE_COMPARATOR;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

import org.springframework.stereotype.Service;

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.table.Cell;
import com.knecon.fforesight.service.layoutparser.processor.model.table.Rectangle;
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.TextPageBlock;
import com.knecon.fforesight.service.layoutparser.processor.utils.DoubleComparisons;
import com.knecon.fforesight.service.layoutparser.processor.utils.RectangularIntersectionFinder;
import com.knecon.fforesight.service.layoutparser.processor.utils.SpreadsheetFinder;

@Service
public class TableExtractionService {

    private static final int MAX_TABLE_CONTAINED_CELLS_WITH_TEXT = 1;
    private static final double TEXT_BLOCK_CONTAINMENT_TOLERANCE = 0.02;
    private static final double TABLE_UNIFORMITY_THRESHOLD = 0.7;


    /**
     * Finds tables on a page and moves textblocks into cells of the found tables.
     * Note: This algorithm uses Pdf Coordinate System where {0,0} rotated with the page rotation.
     * 0 -> LowerLeft
     * 90 -> UpperLeft
     * 180 -> UpperRight
     * 270 -> LowerRight
     * <p>
     * DirAdj (Text direction adjusted) values can not be used here.
     *
     * @param emptyCells The cells used to build the table.
     * @param page       Page object that contains textblocks and statistics.
     */

    public void extractTables(List<Cell> emptyCells, ClassificationPage page) {

        // sort cells by size (height * width) ascending so that textBlocks are always assigned to the smallest cells that contain them
        emptyCells.sort(CELL_SIZE_COMPARATOR);

        for (AbstractPageBlock abstractPageBlock : page.getTextBlocks()) {
            TextPageBlock textBlock = (TextPageBlock) abstractPageBlock;
            for (Cell cell : emptyCells) {
                if (cell.hasMinimumSize() && doesCellContainTextBlock(cell, textBlock)) {
                    cell.addTextBlock(textBlock);
                    break;
                }
            }
        }

        var cells = new ArrayList<>(new HashSet<>(emptyCells));
        DoubleComparisons.sort(cells, Rectangle.ILL_DEFINED_ORDER);

        List<Rectangle> spreadsheetAreas = SpreadsheetFinder.findSpreadsheetsFromCells(cells);
        // sort spreadsheetAreas by size (height * width) ascending so that cells are placed in the smallest tables first
        // this way no cell duplication occurs when tables are contained in other tables and only the most inner table contains the cells
        spreadsheetAreas.sort(RECTANGLE_SIZE_COMPARATOR);

        List<TablePageBlock> tables = new ArrayList<>();
        for (Rectangle area : spreadsheetAreas) {

            List<Cell> containedCells = new ArrayList<>();
            for (Cell c : cells) {
                if (c.hasMinimumSize() && area.contains(c)) {
                    containedCells.add(c);
                }
            }

            var containedCellsWithText = containedCells.stream()
                    .filter(cell -> !cell.getTextBlocks().isEmpty())
                    .toList();

            // verify if table would contain fewer cells with text than the threshold allows
            if (containedCellsWithText.size() >= MAX_TABLE_CONTAINED_CELLS_WITH_TEXT && checkIfTableCellsAreUniform(containedCells)) {
                tables.add(new TablePageBlock(containedCells, area, page.getRotation()));
                cells.removeAll(containedCells);
            }
        }

        for (TablePageBlock table : tables) {
            int position = -1;

            for (AbstractPageBlock pageBlock : page.getTextBlocks()) {
                if (pageBlock instanceof TextPageBlock ? table.containsBlock((TextPageBlock) pageBlock) : table.contains(pageBlock) && position == -1) {
                    position = page.getTextBlocks().indexOf(pageBlock);
                }
            }
            if (position != -1) {
                page.getTextBlocks().add(position, table);

                var toBeRemoved = table.getCells()
                        .stream()
                        .map(Cell::getTextBlocks)
                        .flatMap(List::stream)
                        .toList();
                // remove text blocks from the page that were also added with the table (from its contained cells)
                page.getTextBlocks().removeAll(toBeRemoved);
            }
        }
    }


    private boolean checkIfTableCellsAreUniform(List<Cell> containedCells) {

        if (containedCells.size() <= 2) {
            return true;
        }

        Map<Long, List<Long>> cellsGroupedByRoundedWidth = containedCells.stream()
                .map(Rectangle::getWidth)
                .map(size -> Math.round(size / 10.0) * 10)
                .collect(Collectors.groupingBy(Long::longValue));

        return (double) cellsGroupedByRoundedWidth.size() / containedCells.size() <= TABLE_UNIFORMITY_THRESHOLD;
    }


    private boolean doesCellContainTextBlock(Cell cell, TextPageBlock textBlock) {

        double x = textBlock.getBBox().getX();
        double y = textBlock.getBBox().getY();
        double w = textBlock.getBBox().getWidth();
        double h = textBlock.getBBox().getHeight();
        if (cell.isEmpty() || w <= 0 || h <= 0) {
            return false;
        }
        double x0 = cell.getX();
        double y0 = cell.getY();
        double xTol = TEXT_BLOCK_CONTAINMENT_TOLERANCE * w;
        double yTol = TEXT_BLOCK_CONTAINMENT_TOLERANCE * h;
        return (x >= x0 - xTol && y >= y0 - yTol && (x + w) <= x0 + cell.getWidth() + 2 * xTol && (y + h) <= y0 + cell.getHeight() + 2 * yTol);
    }


    public static List<Cell> findCells(List<Ruling> horizontalRulingLines, List<Ruling> verticalRulingLines) {

        return RectangularIntersectionFinder.find(horizontalRulingLines, verticalRulingLines)
                .stream()
                .map(Cell::new)
                .collect(Collectors.toList());
    }

}
