package com.knecon.fforesight.service.layoutparser.processor.model.table;

import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.TreeMap;

import com.knecon.fforesight.service.layoutparser.processor.model.AbstractPageBlock;
import com.knecon.fforesight.service.layoutparser.processor.model.PageBlockType;
import com.knecon.fforesight.service.layoutparser.processor.model.text.TextPageBlock;
import com.knecon.fforesight.service.layoutparser.processor.utils.RectangleTransformations;

import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;

@Slf4j
public class TablePageBlock extends AbstractPageBlock {

    public static final double CELL_AREA_CONTAINED_THRESHOLD = 0.98;
    private final TreeMap<CellPosition, Cell> cellTreeMap = new TreeMap<>();

    private final int rotation;
    @Getter
    @Setter
    private String headline;
    private int unrotatedRowCount;
    private int unrotatedColCount;
    private List<List<Cell>> rows;
    @Getter
    @Setter
    private List<Cell> cells;


    public TablePageBlock(List<Cell> cells, Rectangle area, int rotation) {

        this.cells = cells;
        addCells(cells);
        minX = area.getLeft();
        minY = area.getBottom();
        maxX = area.getRight();
        maxY = area.getTop();
        classification = PageBlockType.TABLE;
        this.rotation = rotation;
    }


    @Override
    public boolean isEmpty() {

        return getColCount() == 0 || getRowCount() == 0;
    }


    public List<List<Cell>> getRows() {

        if (rows == null) {
            rows = computeRows();

            // Ignore rows that does not contain any cells and values.
            List<List<Cell>> rowsToRemove = new ArrayList<>();
            for (List<Cell> row : rows) {
                if (row.size() == 1 && row.get(0).getTextBlocks().isEmpty()) {
                    rowsToRemove.add(row);
                }
            }
            rows.removeAll(rowsToRemove);

            computeHeaders();
        }

        return rows;

    }


    public int getRowCount() {

        return getRows().size();
    }


    public int getColCount() {

        return getRows().stream()
                .mapToInt(List::size)
                .max()
                .orElse(0);

    }


    /**
     * Detect header cells (either first row or first column):
     * Column is marked as header if originalCell text is bold and row originalCell text is not bold.
     * Defaults to row.
     */
    private void computeHeaders() {

        if (rows == null) {
            rows = computeRows();
        }
        // A bold originalCell is a header originalCell as long as every originalCell to the left/top is bold, too
        // we move from left to right and top to bottom
        for (int rowIndex = 0; rowIndex < rows.size(); rowIndex++) {
            List<Cell> rowCells = rows.get(rowIndex);
            if (rowCells.size() == 1) {
                continue;
            }

            for (int colIndex = 0; colIndex < rowCells.size(); colIndex++) {
                Cell cell = rowCells.get(colIndex);
                List<Cell> cellsToTheLeft = rowCells.subList(0, colIndex);
                Cell lastHeaderCell = null;
                for (Cell leftCell : cellsToTheLeft) {
                    if (leftCell.isHeaderCell()) {
                        lastHeaderCell = leftCell;
                    } else {
                        break;
                    }
                }
                if (lastHeaderCell != null) {
                    cell.getHeaderCells().add(lastHeaderCell);
                }
                List<Cell> cellsToTheTop = new ArrayList<>();
                for (int i = 0; i < rowIndex; i++) {
                    try {
                        cellsToTheTop.add(rows.get(i).get(colIndex));
                    } catch (IndexOutOfBoundsException e) {
                        log.debug("No cell {} in row {}, ignoring.", colIndex, rowIndex);
                    }
                }
                for (Cell topCell : cellsToTheTop) {
                    if (topCell.isHeaderCell()) {
                        lastHeaderCell = topCell;
                    } else {
                        break;
                    }
                }
                if (lastHeaderCell != null) {
                    cell.getHeaderCells().add(lastHeaderCell);
                }
                if (!cell.getTextBlocks().isEmpty() && cell.getTextBlocks().get(0).getMostPopularWordStyle().equals("bold")) {
                    cell.setHeaderCell(true);
                }
            }
        }

    }


    private List<List<Cell>> computeRows() {

        List<List<Cell>> rows = new ArrayList<>();
        if (rotation == 90) {
            for (int i = 0; i < unrotatedColCount; i++) { // rows
                List<Cell> lastRow = new ArrayList<>();
                for (int j = unrotatedRowCount - 1; j >= 0; j--) { // cols
                    Cell cell = cellTreeMap.get(new CellPosition(j, i));
                    if (cell != null) {
                        lastRow.add(cell);
                    }
                }
                rows.add(lastRow);
            }
        } else if (rotation == 270) {
            for (int i = unrotatedColCount - 1; i >= 0; i--) { // rows
                List<Cell> lastRow = new ArrayList<>();
                for (int j = 0; j < unrotatedRowCount; j++) { // cols
                    Cell cell = cellTreeMap.get(new CellPosition(j, i));
                    if (cell != null) {
                        lastRow.add(cell);
                    }
                }
                rows.add(lastRow);
            }
        } else {
            for (int i = 0; i < unrotatedRowCount; i++) {
                List<Cell> lastRow = new ArrayList<>();
                for (int j = 0; j < unrotatedColCount; j++) {
                    Cell cell = cellTreeMap.get(new CellPosition(i, j)); // JAVA_8 use getOrDefault()
                    if (cell != null) {
                        lastRow.add(cell);
                    }
                }
                rows.add(lastRow);
            }
        }

        return rows;

    }


    private void addCells(List<Cell> cells) {

        if (cells.isEmpty()) {
            return;
        }

        cells.removeIf(cell -> cell.getWidth() < 1.1 || cell.getHeight() < 1.1);

        List<List<Cell>> rowsOfCellsMatrix = calculateTableStructure(cells);

        for (int i = 0; i < rowsOfCellsMatrix.size(); i++) {
            for (int j = 0; j < rowsOfCellsMatrix.get(i).size(); j++) {
                addCellToRowAndCol(rowsOfCellsMatrix.get(i).get(j), i, j);
            }
        }

    }


    /**
     * Calculates the structure of the table. For spanning rows and columns multiple cells with the same values will be inserted.
     *
     * @param cells The found cells
     * @return TablePageBlock Structure as a rows of cells matrix
     */
    private List<List<Cell>> calculateTableStructure(List<Cell> cells) {

        if (cells.isEmpty()) {
            return new ArrayList<>();
        }

        Set<Float> uniqueX = new HashSet<>();
        Set<Float> uniqueY = new HashSet<>();
        cells.stream()
                .filter(c -> !c.getTextBlocks().isEmpty() || c.getHeight() > 3 && c.getWidth() > 3)
                .forEach(c -> {
                    uniqueX.add(c.getLeft());
                    uniqueX.add(c.getRight());
                    uniqueY.add(c.getBottom());
                    uniqueY.add(c.getTop());
                });

        var sortedUniqueX = uniqueX.stream()
                .sorted()
                .toList();
        var sortedUniqueY = uniqueY.stream()
                .sorted()
                .toList();

        List<List<Cell>> rowsOfCells = new ArrayList<>();

        Float prevY = null;

        for (Float y : sortedUniqueY) {

            List<Cell> row = new ArrayList<>();

            Float prevX = null;
            for (Float x : sortedUniqueX) {

                if (prevY != null && prevX != null) {
                    var cellFromGridStructure = new Cell(new Point2D.Float(prevX, prevY), new Point2D.Float(x, y));

                    if (cellFromGridStructure.hasMinimumSize()) {

                        cells.stream()
                                .map(originalCell -> new CellWithIntersection(originalCell, RectangleTransformations.calculateIntersectedArea(cellFromGridStructure, originalCell)))
                                .filter(cellWithIntersection -> cellWithIntersection.intersectedArea > 0)
                                .filter(cellWithIntersection -> cellWithIntersection.originalCell.getArea() > cellWithIntersection.intersectedArea * CELL_AREA_CONTAINED_THRESHOLD)
                                .max(Comparator.comparing(CellWithIntersection::intersectedArea))
                                .map(CellWithIntersection::originalCell)
                                .ifPresent(matchingCell -> cellFromGridStructure.getTextBlocks().addAll(matchingCell.getTextBlocks()));

                        row.add(cellFromGridStructure);
                    }
                }
                prevX = x;
            }

            // exclude empty rows and rows where all text blocks are empty
            if (prevY != null && prevX != null && !row.isEmpty() && !row.stream()
                    .allMatch(cell -> cell.getTextBlocks().isEmpty())) {

                rowsOfCells.add(row);
            }
            prevY = y;
        }

        Collections.reverse(rowsOfCells);

        // now cells are removed which are part of a column without any text blocks
        // this is done by first computing the inverse matrix which contains call columns of cells
        // then the column indices that have to be removed are determined
        List<List<Cell>> columnsOfCells = new ArrayList<>();
        int maxRowLength = rowsOfCells.stream()
                .map(List::size)
                .max(java.util.Comparator.naturalOrder())
                .orElse(0);
        for (int i = 0; i < maxRowLength; i++) {
            columnsOfCells.add(new ArrayList<>());
        }

        for (List<Cell> row : rowsOfCells) {
            for (int j = 0; j < row.size(); j++) {
                columnsOfCells.get(j).add(row.get(j));
            }
        }

        List<Integer> columnIndicesToRemove = new ArrayList<>();
        int columnIndex = 0;
        for (List<Cell> col : columnsOfCells) {
            if (col.stream()
                    .allMatch(cell -> cell.getTextBlocks().isEmpty())) {
                columnIndicesToRemove.add(columnIndex);
            }
            columnIndex++;
        }
        columnIndicesToRemove.sort(Collections.reverseOrder());

        // update all rows so that the values of the empty columns get removed
        var rowsOfCellsBefore = new ArrayList<>(rowsOfCells);
        rowsOfCells = new ArrayList<>();
        for (List<Cell> row : rowsOfCellsBefore) {
            var updatedRow = new ArrayList<>(row);
            columnIndicesToRemove.forEach(idxToRemove -> updatedRow.remove(updatedRow.get(idxToRemove)));
            rowsOfCells.add(updatedRow);
        }

        return rowsOfCells;
    }


    private void addCellToRowAndCol(Cell cell, int row, int col) {

        unrotatedRowCount = Math.max(unrotatedRowCount, row + 1);
        unrotatedColCount = Math.max(unrotatedColCount, col + 1);

        CellPosition cp = new CellPosition(row, col);
        cellTreeMap.put(cp, cell);

    }


    @Override
    public String getText() {

        StringBuilder sb = new StringBuilder();
        List<List<Cell>> rows = getRows();

        int i = 0;
        for (List<Cell> row : rows) {
            if (i != 0) {
                sb.append("\n");
            }
            if (!row.isEmpty()) {
                boolean firstColumn = true;
                for (Cell column : row) {
                    if (!firstColumn) {
                        sb.append(",");
                    }
                    if (column != null && column.getTextBlocks() != null) {
                        boolean first = true;
                        for (TextPageBlock textBlock : column.getTextBlocks()) {
                            if (!first) {
                                sb.append("\n");
                            }
                            sb.append('\"').append(textBlock.getText().replaceAll("\"", "\"")).append('\"');
                            first = false;
                        }
                    }
                    firstColumn = false;
                }
            }
            i++;
        }

        return sb.toString();
    }


    public String getTextAsHtml() {

        StringBuilder sb = new StringBuilder();
        List<List<Cell>> rows = getRows();

        sb.append("<table border=\"1\">");
        int i = 0;
        for (List<Cell> row : rows) {
            sb.append("\n<tr>");
            if (!row.isEmpty()) {
                for (Cell column : row) {
                    sb.append(i == 0 ? "\n<th>" : "\n<td>");
                    if (column != null && column.getTextBlocks() != null) {
                        boolean first = true;
                        for (TextPageBlock textBlock : column.getTextBlocks()) {
                            if (!first) {
                                sb.append("<br />");
                            }
                            sb.append(textBlock.getText().replaceAll("\\n", "<br />"));
                            first = false;
                        }
                    }
                    sb.append(i == 0 ? "</th>" : "</td>");
                }
            }
            sb.append("</tr>");
            i++;
        }
        sb.append("</table>");

        return sb.toString();
    }


    public Rectangle2D getBBox() {

        if (this.bBox == null) {
            this.bBox = cells.stream()
                    .collect(RectangleTransformations.collectBBox());
        }
        return this.bBox;
    }


    record CellWithIntersection(Cell originalCell, double intersectedArea) {

    }

}