package com.knecon.fforesight.service.layoutparser.processor.services.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.io.File;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import org.springframework.stereotype.Service;

import com.knecon.fforesight.service.layoutparser.internal.api.data.redaction.LayoutEngine;
import com.knecon.fforesight.service.layoutparser.internal.api.data.redaction.NodeType;
import com.knecon.fforesight.service.layoutparser.processor.model.graph.nodes.Document;
import com.knecon.fforesight.service.layoutparser.processor.model.graph.nodes.DuplicatedParagraph;
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.utils.RectangleTransformations;
import com.knecon.fforesight.service.viewerdoc.ContentStreams;
import com.knecon.fforesight.service.viewerdoc.model.ColoredLine;
import com.knecon.fforesight.service.viewerdoc.model.ColoredRectangle;
import com.knecon.fforesight.service.viewerdoc.model.LayoutGrid;
import com.knecon.fforesight.service.viewerdoc.model.PlacedText;
import com.knecon.fforesight.service.viewerdoc.model.Standard14EmbeddableFont;
import com.knecon.fforesight.service.viewerdoc.model.Visualizations;
import com.knecon.fforesight.service.viewerdoc.service.ViewerDocumentService;

import io.micrometer.observation.annotation.Observed;
import lombok.AccessLevel;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.experimental.FieldDefaults;

@Service
@RequiredArgsConstructor
@FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true)
public class LayoutGridService {

    ViewerDocumentService viewerDocumentService;

    static float FONT_SIZE = 10f;
    static float LINE_WIDTH = 1f;
    static Standard14EmbeddableFont FONT = Standard14EmbeddableFont.helvetica();

    static Color INNER_LINES_COLOR = new Color(255, 175, 175);
    static Color PARAGRAPH_COLOR = new Color(70, 130, 180);

    static Color DUPLICATE_PARAGRAPH_COLOR = new Color(70, 180, 101);
    static Color TABLE_COLOR = new Color(102, 205, 170);
    static Color SECTION_COLOR = new Color(50, 50, 50);
    static Color HEADLINE_COLOR = new Color(162, 56, 56);
    static Color HEADER_COLOR = new Color(171, 131, 6);
    static Color IMAGE_COLOR = new Color(253, 63, 146);


    @SneakyThrows
    @Observed(name = "ViewerDocumentService", contextualName = "create-viewer-document")
    public void addLayoutGrid(File originFile, Document document, File destinationFile, boolean layerVisibilityDefaultValue, boolean writeVisualLayoutParsingGrid) {

        List<Visualizations> allVisualizations;
        Visualizations layoutGrid = this.addLayoutGrid(document, layerVisibilityDefaultValue, false);
        if (writeVisualLayoutParsingGrid) {
            Visualizations visualLayoutGrid = this.addLayoutGrid(document, layerVisibilityDefaultValue, true);
            allVisualizations = Stream.concat(Stream.of(layoutGrid, visualLayoutGrid), document.getVisualizations().streamAll())
                    .toList();
        } else {
            allVisualizations = Stream.concat(Stream.of(layoutGrid), document.getVisualizations().streamAll())
                    .toList();
        }

        viewerDocumentService.addVisualizationsOnPage(originFile, destinationFile, allVisualizations);
    }


    @SneakyThrows
    @Observed(name = "ViewerDocumentService", contextualName = "create-viewer-document")
    public Visualizations addLayoutGrid(Document document, boolean layerVisibilityDefaultValue, boolean visualParsingGrid) {

        LayoutGrid layoutGrid = createLayoutGrid(document, visualParsingGrid);

        return Visualizations.builder()
                .layer(visualParsingGrid ? ContentStreams.KNECON_VISUAL_PARSING : ContentStreams.KNECON_LAYOUT)
                .visualizationsOnPages(layoutGrid.getVisualizationsPerPages())
                .layerVisibilityDefaultValue(layerVisibilityDefaultValue)
                .build();
    }


    private LayoutGrid createLayoutGrid(Document document, boolean visualParsingGrid) {

        LayoutGrid layoutGrid = new LayoutGrid(document.getNumberOfPages());
        document.streamAllSubNodes()
                .filter(node -> (node.getEngines().contains(LayoutEngine.AI) && visualParsingGrid) || (node.getEngines().contains(LayoutEngine.ALGORITHM) && !visualParsingGrid))
                .forEach(semanticNode -> {
                    Color color = switch (semanticNode.getType()) {
                        case PARAGRAPH -> PARAGRAPH_COLOR;
                        case TABLE -> TABLE_COLOR;
                        case SECTION -> SECTION_COLOR;
                        case HEADLINE -> HEADLINE_COLOR;
                        case HEADER, FOOTER -> HEADER_COLOR;
                        case IMAGE -> IMAGE_COLOR;
                        default -> null;
                    };

                    if (semanticNode instanceof DuplicatedParagraph) {
                        color = DUPLICATE_PARAGRAPH_COLOR;
                    }

                    if (isNotSectionOrTableCellOrDocument(semanticNode)) {
                        addAsRectangle(semanticNode, layoutGrid, color);
                    }
                    if (semanticNode.getType().equals(NodeType.SECTION)) {
                        addSection(semanticNode, layoutGrid, color);
                    }
                    if (semanticNode.getType().equals(NodeType.TABLE)) {
                        Table table = (Table) semanticNode;
                        addInnerTableLines(table, layoutGrid);
                    }
                });
        return layoutGrid;
    }


    private void addInnerTableLines(Table table, LayoutGrid layoutGrid) {

        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 = layoutGrid.getVisualizationsPerPages().get(page.getNumber() - 1).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 addSection(SemanticNode semanticNode, LayoutGrid layoutGrid, Color color) {

        Map<Page, Rectangle2D> bBoxMap = semanticNode.getBBox();
        List<SemanticNode> subSections = semanticNode.streamAllSubNodesOfType(NodeType.SECTION)
                .toList();
        Page firstPage = semanticNode.getFirstPage();
        if (!subSections.isEmpty()) {
            addPlacedText(firstPage, bBoxMap.get(firstPage), buildTreeIdString(semanticNode), layoutGrid);
        } else {
            bBoxMap.forEach(((page, textBBox) -> addPlacedText(page, textBBox, buildTreeIdString(semanticNode), layoutGrid)));
        }
        if (bBoxMap.values().size() == 1) {
            Rectangle2D r = RectangleTransformations.pad(bBoxMap.get(firstPage), LINE_WIDTH, LINE_WIDTH);
            List<ColoredLine> coloredLines = layoutGrid.getVisualizationsPerPages().get(firstPage.getNumber() - 1).getColoredLines();
            List<Line2D> lines = createLinesFromRectangle(r, firstPage.getRotation());
            // add string to top line
            var firstLine = lines.remove(0);
            coloredLines.add(new ColoredLine(firstLine, color, LINE_WIDTH));
            for (Line2D line : lines) {
                coloredLines.add(new ColoredLine(line, color, LINE_WIDTH));
            }
            return;
        }
        List<Page> pagesInOrder = bBoxMap.keySet()
                .stream()
                .sorted(Comparator.comparingInt(Page::getNumber))
                .collect(Collectors.toList());
        pagesInOrder.remove(0);
        addLinesForFirstPageOfSection(semanticNode, color, firstPage, layoutGrid);
        var lastPage = pagesInOrder.remove(pagesInOrder.size() - 1);
        addLinesForLastPageOfSection(semanticNode, color, lastPage, layoutGrid);
        for (Page middlePage : pagesInOrder) {
            addLinesForMiddlePageOfSection(semanticNode, color, middlePage, layoutGrid);
        }
    }


    @SneakyThrows
    private void addPlacedText(Page page, Rectangle2D textBBox, String s, LayoutGrid layoutGrid) {

        // 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 + (2 * LINE_WIDTH) + 4);

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

        upperLeftCorner = add(upperLeftCorner, translationVector);

        var placedTexts = layoutGrid.getVisualizationsPerPages().get(page.getNumber() - 1).getPlacedTexts();
        placedTexts.add(PlacedText.textFacingUp(s, upperLeftCorner, FONT_SIZE, Color.BLACK, FONT));
    }


    private void addLinesForMiddlePageOfSection(SemanticNode semanticNode, Color color, Page middlePage, LayoutGrid layoutGrid) {

        List<ColoredLine> coloredLines = layoutGrid.getVisualizationsPerPages().get(middlePage.getNumber() - 1).getColoredLines();
        Rectangle2D r = RectangleTransformations.pad(semanticNode.getBBox().get(middlePage), LINE_WIDTH, LINE_WIDTH);
        var midPageLines = createLinesFromRectangle(r, middlePage.getRotation());
        // remove top line
        midPageLines.remove(0);
        // remove top line
        midPageLines.remove(1);
        // add string to left line
        var leftLine = midPageLines.remove(1);
        coloredLines.add(new ColoredLine(leftLine, color, LINE_WIDTH));
        for (Line2D line : midPageLines) {
            coloredLines.add(new ColoredLine(line, color, LINE_WIDTH));
        }
    }


    private void addLinesForLastPageOfSection(SemanticNode semanticNode, Color color, Page lastPage, LayoutGrid layoutGrid) {

        List<ColoredLine> coloredLines = layoutGrid.getVisualizationsPerPages().get(lastPage.getNumber() - 1).getColoredLines();
        Rectangle2D r = RectangleTransformations.pad(semanticNode.getBBox().get(lastPage), LINE_WIDTH, LINE_WIDTH);
        var lastPageLines = createLinesFromRectangle(r, lastPage.getRotation());
        // remove top line
        lastPageLines.remove(0);
        // add string to left line
        var leftLine = lastPageLines.remove(2);
        coloredLines.add(new ColoredLine(leftLine, color, LINE_WIDTH));
        for (Line2D line : lastPageLines) {
            coloredLines.add(new ColoredLine(line, color, LINE_WIDTH));
        }
    }


    private void addLinesForFirstPageOfSection(SemanticNode semanticNode, Color color, Page firstPage, LayoutGrid layoutGrid) {

        List<ColoredLine> coloredLines = layoutGrid.getVisualizationsPerPages().get(firstPage.getNumber() - 1).getColoredLines();
        Rectangle2D r = RectangleTransformations.pad(semanticNode.getBBox().get(firstPage), LINE_WIDTH, LINE_WIDTH);
        var firstPageLines = createLinesFromRectangle(r, firstPage.getRotation());
        // remove bottom line
        firstPageLines.remove(2);
        // add string to top line
        var firstLine = firstPageLines.remove(0);
        coloredLines.add(new ColoredLine(firstLine, color, LINE_WIDTH));
        for (Line2D line : firstPageLines) {
            coloredLines.add(new ColoredLine(line, color, LINE_WIDTH));
        }
    }


    private String buildTreeIdString(SemanticNode semanticNode) {

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


    /*
    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 static boolean isNotSectionOrTableCellOrDocument(SemanticNode semanticNode) {

        return !(semanticNode.getType().equals(NodeType.DOCUMENT) || semanticNode.getType().equals(NodeType.SECTION) || semanticNode.getType().equals(NodeType.TABLE_CELL));
    }


    private void addAsRectangle(SemanticNode semanticNode, LayoutGrid layoutGrid, Color color) {

        semanticNode.getBBox()
                .forEach((page, textBBox) -> layoutGrid.getVisualizationsPerPages()
                        .get(page.getNumber() - 1)
                        .getColoredRectangles()
                        .add(new ColoredRectangle(textBBox, color, LINE_WIDTH)));
    }


    private Point2D add(Point2D a, Point2D b) {

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

}
