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

import java.awt.Color;
import java.awt.geom.Line2D;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.awt.geom.RectangularShape;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import com.knecon.fforesight.service.layoutparser.internal.api.data.redaction.LayoutEngine;
import com.knecon.fforesight.service.layoutparser.internal.api.data.redaction.NodeTypeProto;
import com.knecon.fforesight.service.layoutparser.processor.model.graph.nodes.DuplicatedParagraph;
import com.knecon.fforesight.service.layoutparser.processor.model.graph.nodes.Headline;
import com.knecon.fforesight.service.layoutparser.processor.model.graph.nodes.Image;
import com.knecon.fforesight.service.layoutparser.processor.model.graph.nodes.Page;
import com.knecon.fforesight.service.layoutparser.processor.model.graph.nodes.Paragraph;
import com.knecon.fforesight.service.layoutparser.processor.model.graph.nodes.SemanticNode;
import com.knecon.fforesight.service.layoutparser.processor.model.graph.nodes.SuperSection;
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.utils.RectangleTransformations;
import com.knecon.fforesight.service.viewerdoc.layers.LayoutGridLayerConfig;
import com.knecon.fforesight.service.viewerdoc.model.ColoredLine;
import com.knecon.fforesight.service.viewerdoc.model.ColoredRectangle;
import com.knecon.fforesight.service.viewerdoc.model.FilledRectangle;
import com.knecon.fforesight.service.viewerdoc.model.PlacedText;
import com.knecon.fforesight.service.viewerdoc.model.Visualizations;

import lombok.AccessLevel;
import lombok.Getter;
import lombok.Setter;
import lombok.SneakyThrows;
import lombok.experimental.FieldDefaults;

@FieldDefaults(level = AccessLevel.PRIVATE)
public class LayoutGrid extends LayoutGridLayerConfig {

    @Getter
    @Setter
    boolean visibleByDefault;

    final HashMap<RectangleIdentifier, Rectangle2D> rectangleMap = new HashMap<>();


    public void addParagraph(Paragraph paragraph) {

        if (paragraph instanceof DuplicatedParagraph) {
            addAsRectangle(paragraph, paragraphs, DUPLICATE_PARAGRAPH_COLOR);
        } else {
            addAsRectangle(paragraph, paragraphs, PARAGRAPH_COLOR);
        }
    }


    public void addImage(Image image) {

        if (image.isFullPageImage()) {
            addAsRectangle(image, images, IMAGE_COLOR);
        } else {
            addAsRectangle(image, figures, IMAGE_COLOR);
        }
    }


    public void addHeadline(Headline headline) {

        addAsRectangle(headline, headlines, HEADLINE_COLOR);
        if (headline.getEngines().contains(LayoutEngine.OUTLINE)) {
            addAsRectangle(headline, outlineHeadlines, HEADLINE_COLOR);
        }
    }


    public void addHeaderOrFooter(SemanticNode header) {

        addAsRectangle(header, headerFooter, HEADER_COLOR);
    }


    public void addTreeId(SemanticNode semanticNode) {

        Page page = semanticNode.getFirstPage();
        if (semanticNode.getBBox()
                    .get(page) == null) {
            return;
        }
        addPlacedText(page,
                      semanticNode.getBBox()
                              .get(page),
                      semanticNode.getBBox()
                              .get(page),
                      buildTreeIdString(semanticNode),
                      1,
                      treeIds,
                      TREEID_COLOR);
    }


    public void addTable(Table table) {

        addAsRectangle(table, tables, TABLE_COLOR);
        addInnerTableLines(table);
        addHeaderCells(table);
    }


    private void addHeaderCells(Table table) {

        table.streamHeaders()
                .map(TableCell::getBBox)
                .forEach(map -> map.forEach((page, textBBox) -> getOrCreateVisualizationsOnPage(page.getNumber(), tables).getFilledRectangles()
                        .add(new FilledRectangle(textBBox, HEADER_CELL_COLOR, 0.1f))));
    }


    public void addSection(SemanticNode section) {

        Map<Page, Rectangle2D> bBoxMap = section.getBBox();

        List<SemanticNode> subSections = section.streamAllSubNodesOfType(NodeTypeProto.NodeType.SECTION)
                .toList();
        Integer maxChildDepth = subSections.stream()
                .map(node -> node.getTreeId().size())
                .max(Integer::compareTo)
                .orElse(section.getTreeId().size());
        int ownDepth = section.getTreeId().size();

        Page firstPage = section.getFirstPage();
        String treeIdString = buildTreeIdString(section);

        if (bBoxMap.values().size() == 1) {
            handleSinglePage(section, firstPage, bBoxMap.get(firstPage), treeIdString, maxChildDepth, ownDepth);
            return;
        }
        List<Page> pagesInOrder = bBoxMap.keySet()
                .stream()
                .sorted(Comparator.comparingInt(Page::getNumber))
                .collect(Collectors.toList());
        pagesInOrder.remove(0);
        handleFirstPageOfSection(section, firstPage, bBoxMap.get(firstPage), treeIdString, maxChildDepth, ownDepth);
        for (Page middlePage : pagesInOrder.subList(0, pagesInOrder.size() - 1)) {
            handleForMiddlePageOfSection(section, middlePage, bBoxMap.get(middlePage), treeIdString, maxChildDepth, ownDepth);
        }
        var lastPage = pagesInOrder.remove(pagesInOrder.size() - 1);
        handleLastPageOfSection(section, lastPage, bBoxMap.get(lastPage), treeIdString, maxChildDepth, ownDepth);
    }


    private String buildTreeIdString(SemanticNode semanticNode) {

        return semanticNode.getTreeId()
                .stream()
                .map(Object::toString)
                .collect(Collectors.joining("."));
    }


    @SneakyThrows
    private void addPlacedText(Page page, Rectangle2D textBBox, Rectangle2D highestParentRect, String s, Integer maxChildDepth, Visualizations visualizations, Color color) {

        // translates text, such that its right edge is a bit to the left of the drawn box
        float translationAmount = ((FONT.getStringWidth(s) / 1000) * FONT_SIZE + LINE_WIDTH + 2 * maxChildDepth);

        Point2D upperLeftCorner;
        Point2D translationVector;
        switch (page.getRotation()) {
            case 90 -> {
                if (highestParentRect != null) {
                    upperLeftCorner = new Point2D.Double(highestParentRect.getMinX(), textBBox.getMinY());
                } else {
                    upperLeftCorner = new Point2D.Double(textBBox.getMinX(), textBBox.getMinY());
                }
                translationVector = new Point2D.Double(FONT_SIZE, -translationAmount);
            }
            case 180 -> {
                if (highestParentRect != null) {
                    upperLeftCorner = new Point2D.Double(highestParentRect.getMaxX(), textBBox.getMinY());
                } else {
                    upperLeftCorner = new Point2D.Double(textBBox.getMaxX(), textBBox.getMinY());
                }
                translationVector = new Point2D.Double(translationAmount, FONT_SIZE);
            }
            case 270 -> {

                if (highestParentRect != null) {
                    upperLeftCorner = new Point2D.Double(highestParentRect.getMaxX(), textBBox.getMaxY());
                } else {
                    upperLeftCorner = new Point2D.Double(textBBox.getMaxX(), textBBox.getMaxY());
                }
                translationVector = new Point2D.Double(-FONT_SIZE, translationAmount);
            }
            default -> {

                if (highestParentRect != null) {
                    upperLeftCorner = new Point2D.Double(highestParentRect.getMinX(), textBBox.getMaxY());
                } else {
                    upperLeftCorner = new Point2D.Double(textBBox.getMinX(), textBBox.getMaxY());
                }
                translationVector = new Point2D.Double(-translationAmount, -FONT_SIZE);
            }
        }

        upperLeftCorner = add(upperLeftCorner, translationVector);

        List<PlacedText> placedTexts = getOrCreateVisualizationsOnPage(page.getNumber(), visualizations).getPlacedTexts();

        PlacedText newText = PlacedText.textFacingUp(s, upperLeftCorner, FONT_SIZE, color, FONT);
        float threshold = 1.5f * FONT_SIZE;
        Optional<PlacedText> conflictingText = placedTexts.stream()
                .filter(pt -> Math.abs(pt.lineStart().getY() - newText.lineStart().getY()) <= threshold
                              && Math.abs(pt.lineStart().getX() - newText.lineStart().getX()) <= threshold)
                .findFirst();

        if (conflictingText.isPresent()) {
            PlacedText existingText = conflictingText.get();
            if (newText.text().length() > existingText.text().length()) {
                placedTexts.remove(existingText);
                placedTexts.add(newText);
            }
        } else {
            placedTexts.add(newText);
        }

    }


    private void handleSinglePage(SemanticNode semanticNode, Page page, Rectangle2D rectangle2D, String treeIdString, Integer maxChildDepth, Integer ownDepth) {

        RectangleAndLinesResult result = createLinesAndPlaceText(semanticNode, page, rectangle2D, treeIdString, maxChildDepth, ownDepth);
        // add string to top line
        var firstLine = result.pageLines().remove(0);
        result.coloredLines().add(new ColoredLine(firstLine, SECTION_COLOR, LINE_WIDTH));
        for (Line2D line : result.pageLines()) {
            result.coloredLines().add(new ColoredLine(line, SECTION_COLOR, LINE_WIDTH));
        }
    }


    private void handleFirstPageOfSection(SemanticNode semanticNode, Page firstPage, Rectangle2D rectangle2D, String treeIdString, Integer maxChildDepth, Integer ownDepth) {

        RectangleAndLinesResult result = createLinesAndPlaceText(semanticNode, firstPage, rectangle2D, treeIdString, maxChildDepth, ownDepth);
        // remove bottom line
        result.pageLines().remove(2);
        // add string to top line
        var firstLine = result.pageLines().remove(0);
        result.coloredLines().add(new ColoredLine(firstLine, SECTION_COLOR, LINE_WIDTH));
        for (Line2D line : result.pageLines()) {
            result.coloredLines().add(new ColoredLine(line, SECTION_COLOR, LINE_WIDTH));
        }
    }


    private void handleForMiddlePageOfSection(SemanticNode semanticNode, Page middlePage, Rectangle2D rectangle2D, String treeIdString, Integer maxChildDepth, Integer ownDepth) {

        RectangleAndLinesResult result = createLinesAndPlaceText(semanticNode, middlePage, rectangle2D, treeIdString, maxChildDepth, ownDepth);
        // remove top line
        result.pageLines().remove(0);
        // remove bottom line
        result.pageLines().remove(1);
        // add string to left line
        var leftLine = result.pageLines().remove(1);
        result.coloredLines().add(new ColoredLine(leftLine, SECTION_COLOR, LINE_WIDTH));
        for (Line2D line : result.pageLines()) {
            result.coloredLines().add(new ColoredLine(line, SECTION_COLOR, LINE_WIDTH));
        }
    }


    private void handleLastPageOfSection(SemanticNode semanticNode, Page lastPage, Rectangle2D rectangle2D, String treeIdString, Integer maxChildDepth, Integer ownDepth) {

        RectangleAndLinesResult result = createLinesAndPlaceText(semanticNode, lastPage, rectangle2D, treeIdString, maxChildDepth, ownDepth);
        // remove top line
        result.pageLines().remove(0);
        // add string to left line
        var leftLine = result.pageLines().remove(2);
        result.coloredLines().add(new ColoredLine(leftLine, SECTION_COLOR, LINE_WIDTH));
        for (Line2D line : result.pageLines()) {
            result.coloredLines().add(new ColoredLine(line, SECTION_COLOR, LINE_WIDTH));
        }
    }


    private RectangleAndLinesResult createLinesAndPlaceText(SemanticNode semanticNode,
                                                            Page page,
                                                            Rectangle2D rectangle2D,
                                                            String treeIdString,
                                                            Integer maxChildDepth,
                                                            Integer ownDepth) {

        List<ColoredLine> coloredLines = getOrCreateVisualizationsOnPage(page.getNumber(), sections).getColoredLines();
        int lineWidthModifier = maxChildDepth - ownDepth;
        Rectangle2D r = RectangleTransformations.pad(semanticNode.getBBox()
                                                             .get(page), LINE_WIDTH * (1 + lineWidthModifier), LINE_WIDTH * (1 + lineWidthModifier));

        SemanticNode highestParent = semanticNode.getHighestParent();
        Rectangle2D highestParentRect = rectangleMap.get(new RectangleIdentifier(highestParent.getTreeId(), page.getNumber()));
        addPlacedText(page, rectangle2D, highestParentRect, treeIdString, maxChildDepth, sections, SECTION_COLOR);
        var lastPageLines = createLinesFromRectangle(r, page.getRotation());

        if (semanticNode instanceof SuperSection) {
            rectangleMap.put(new RectangleIdentifier(semanticNode.getTreeId(), page.getNumber()), r);
        }

        return new RectangleAndLinesResult(coloredLines, r, lastPageLines);
    }


    private void addInnerTableLines(Table table) {

        if (table.getNumberOfCols() < 1 || table.getNumberOfRows() < 1) {
            return;
        }
        for (Page page : table.getPages()) {

            Optional<Integer> optionalFirstRowOnPage = table.streamCol(0)
                    .filter(tableCell -> tableCell.isOnPage(page.getNumber()))
                    .map(TableCell::getRow)
                    .findFirst();
            if (optionalFirstRowOnPage.isEmpty()) {
                continue;
            }
            int firstRowOnPage = optionalFirstRowOnPage.get();

            Stream<Double> xStream = switch (page.getRotation()) {
                case 90 -> streamBBoxOfCellsOnPage(table.streamCol(0), page).map(RectangularShape::getMinX);
                case 180 -> streamBBoxOfCellsOnPage(table.streamRow(firstRowOnPage), page).map(RectangularShape::getMaxX);
                case 270 -> streamBBoxOfCellsOnPage(table.streamCol(0), page).map(RectangularShape::getMaxX);
                default -> streamBBoxOfCellsOnPage(table.streamRow(firstRowOnPage), page).map(RectangularShape::getMinX);
            };
            List<Double> xs = xStream.collect(Collectors.toList());
            xs.remove(0);

            Stream<Double> yStream = switch (page.getRotation()) {
                case 90 -> streamBBoxOfCellsOnPage(table.streamRow(firstRowOnPage), page).map(RectangularShape::getMinY);
                case 180 -> streamBBoxOfCellsOnPage(table.streamCol(0), page).map(RectangularShape::getMinY);
                case 270 -> streamBBoxOfCellsOnPage(table.streamRow(firstRowOnPage), page).map(RectangularShape::getMaxY);
                default -> streamBBoxOfCellsOnPage(table.streamCol(0), page).map(RectangularShape::getMaxY);
            };
            List<Double> ys = yStream.collect(Collectors.toList());
            ys.remove(0);

            Rectangle2D tableBBox = table.getBBox()
                    .get(page);
            List<ColoredLine> coloredLines = getOrCreateVisualizationsOnPage(page.getNumber(), tables).getColoredLines();

            xs.forEach(x -> {
                Line2D line = new Line2D.Double(new Point2D.Double(x, tableBBox.getMaxY()), new Point2D.Double(x, tableBBox.getMinY()));
                coloredLines.add(new ColoredLine(line, INNER_LINES_COLOR, LINE_WIDTH));
            });
            ys.forEach(y -> {
                Line2D line = new Line2D.Double(new Point2D.Double(tableBBox.getMinX(), y), new Point2D.Double(tableBBox.getMaxX(), y));
                coloredLines.add(new ColoredLine(line, INNER_LINES_COLOR, LINE_WIDTH));
            });
        }
    }


    private static Stream<Rectangle2D> streamBBoxOfCellsOnPage(Stream<TableCell> table, Page page) {

        return table.filter(tableCell -> tableCell.isOnPage(page.getNumber()))
                .map(TableCell::getBBox)
                .map(bBoxMap -> bBoxMap.get(page));
    }


    private void addAsRectangle(SemanticNode semanticNode, Visualizations visualizations, Color color) {

        addAsRectangle(semanticNode.getBBox(), visualizations, color);
    }


    private void addAsRectangle(Map<Page, Rectangle2D> bbox, Visualizations visualizations, Color color) {

        bbox.forEach((page, textBBox) -> getOrCreateVisualizationsOnPage(page.getNumber(), visualizations).getColoredRectangles()
                .add(new ColoredRectangle(textBBox, color, LINE_WIDTH)));
    }


    private record RectangleAndLinesResult(List<ColoredLine> coloredLines, Rectangle2D rectangle, List<Line2D> pageLines) {

    }

    private record RectangleIdentifier(List<Integer> treeId, Integer pageNumber) {

    }


    /*
    A __________________ B
     |                  |
     |                  |
     |                  |
     |                  |
    D|__________________| C
    The returned List are the lines [AB, BC, DC, AD]
    The List is reordered, such that the order of the returned lines are always as viewed on the page.
     */
    private List<Line2D> createLinesFromRectangle(Rectangle2D r, int pageRotation) {
        // +0.5 to join the lines
        List<Line2D> lines = new ArrayList<>(4);
        float lineWidthCorrection = LINE_WIDTH * 0.5f;
        Point2D.Float a = new Point2D.Float((float) r.getMinX(), (float) r.getMaxY());
        Point2D.Float a1 = new Point2D.Float((float) r.getMinX() - lineWidthCorrection, (float) r.getMaxY());
        Point2D.Float b = new Point2D.Float((float) r.getMaxX(), (float) r.getMaxY());
        Point2D.Float b1 = new Point2D.Float((float) r.getMaxX() + lineWidthCorrection, (float) r.getMaxY());
        Point2D.Float c = new Point2D.Float((float) r.getMaxX(), (float) r.getMinY());
        Point2D.Float c1 = new Point2D.Float((float) r.getMaxX() + lineWidthCorrection, (float) r.getMinY());
        Point2D.Float d = new Point2D.Float((float) r.getMinX(), (float) r.getMinY());
        Point2D.Float d1 = new Point2D.Float((float) r.getMinX() - lineWidthCorrection, (float) r.getMinY());
        lines.add(new Line2D.Float(a1, b1));
        lines.add(new Line2D.Float(b, c));
        lines.add(new Line2D.Float(d1, c1));
        lines.add(new Line2D.Float(a, d));

        return switch (pageRotation) {
            case 90 -> {
                Collections.rotate(lines, 1);
                yield lines;
            }
            case 180 -> {
                Collections.rotate(lines, 2);
                yield lines;
            }
            case 270 -> {
                Collections.rotate(lines, 3);
                yield lines;
            }

            default -> lines;
        };
    }


    private Point2D add(Point2D a, Point2D b) {

        return new Point2D.Double(a.getX() + b.getX(), a.getY() + b.getY());
    }

}
