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

import java.awt.Color;
import java.awt.geom.Line2D;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.io.IOException;
import java.util.List;
import java.util.Map;

import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.PDPageContentStream;
import org.apache.pdfbox.pdmodel.font.PDType1Font;
import org.apache.pdfbox.pdmodel.font.Standard14Fonts;

import com.knecon.fforesight.service.layoutparser.internal.api.data.redaction.NodeType;
import com.knecon.fforesight.service.layoutparser.processor.model.graph.DocumentTree;
import com.knecon.fforesight.service.layoutparser.processor.model.graph.nodes.Document;
import com.knecon.fforesight.service.layoutparser.processor.model.graph.nodes.Page;
import com.knecon.fforesight.service.layoutparser.processor.model.graph.textblock.AtomicTextBlock;
import com.knecon.fforesight.service.layoutparser.processor.model.graph.textblock.TextBlock;

import lombok.SneakyThrows;
import lombok.experimental.UtilityClass;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@UtilityClass
public class PdfVisualisationUtility {

    public void drawDocumentGraph(PDDocument document, Document documentGraph) {

        documentGraph.getDocumentTree().allEntriesInOrder().forEach(entry -> drawNode(document, entry));
    }


    public void drawNode(PDDocument document, DocumentTree.Entry entry) {

        DrawingOptions options = buildStandardOptionsForNodes(entry);

        drawBBoxAndLabelAndNumberOnPage(document, entry, options);

    }


    public void drawTextBlock(PDDocument document, TextBlock textBlock, DrawingOptions options) {

        textBlock.getAtomicTextBlocks().forEach(atb -> drawAtomicTextBlock(document, atb, options));
    }


    public void drawAtomicTextBlock(PDDocument document, AtomicTextBlock atomicTextBlock, DrawingOptions options) {

        drawRectangle2DList(document, atomicTextBlock.getPage().getNumber(), atomicTextBlock.getPositions().stream().toList(), options);

    }


    @SneakyThrows
    public void drawText(String string, PDDocument document, Point2D location, Integer pageNumber, DrawingOptions options) {

        var pdPage = document.getPage(pageNumber - 1);
        var contentStream = new PDPageContentStream(document, pdPage, PDPageContentStream.AppendMode.APPEND, true);

        contentStream.setNonStrokingColor(options.getStrokeColor());
        contentStream.setLineWidth(options.getStrokeWidth());

        contentStream.beginText();
        contentStream.newLineAtOffset((float) location.getX(), (float) location.getY());
        contentStream.setFont(new PDType1Font(Standard14Fonts.FontName.HELVETICA), 10);
        contentStream.showText(string);
        contentStream.endText();
        contentStream.close();
    }


    @SneakyThrows
    public void drawRectangle2DList(PDDocument document, int pageNumber, List<Rectangle2D> rectCollection, DrawingOptions options) {

        var pdPage = document.getPage(pageNumber - 1);
        drawRectangle2DList(document, rectCollection, options, pdPage);
    }


    private void drawRectangle2DList(PDDocument document, List<Rectangle2D> rectCollection, DrawingOptions options, PDPage pdPage) throws IOException {

        var contentStream = new PDPageContentStream(document, pdPage, PDPageContentStream.AppendMode.APPEND, true);

        contentStream.setStrokingColor(options.getStrokeColor());
        contentStream.setNonStrokingColor(options.getFillColor());
        contentStream.setLineWidth(options.getStrokeWidth());

        for (var r : rectCollection) {
            contentStream.addRect((float) r.getMinX(), (float) r.getMinY(), (float) r.getWidth(), (float) r.getHeight());

            if (options.isStroke() && options.isFill()) {
                contentStream.fillAndStroke();
            } else if (options.isStroke()) {
                contentStream.stroke();
            } else if (options.isFill()) {
                contentStream.fill();
            }
        }
        contentStream.close();
    }


    private DrawingOptions buildStandardOptionsForNodes(DocumentTree.Entry entry) {

        return DrawingOptions.builder().stroke(true).strokeColor(switch (entry.getType()) {
            case DOCUMENT -> Color.LIGHT_GRAY;
            case HEADER, FOOTER -> Color.GREEN;
            case PARAGRAPH -> Color.BLUE;
            case HEADLINE -> Color.RED;
            case SECTION -> Color.BLACK;
            case TABLE -> Color.ORANGE;
            case TABLE_CELL -> Color.GRAY;
            case IMAGE -> Color.MAGENTA;
        }).build();
    }


    private void drawBBoxAndLabelAndNumberOnPage(PDDocument document, DocumentTree.Entry entry, DrawingOptions options) {

        Map<Page, Rectangle2D> rectanglesPerPage = entry.getNode().getBBox();
        rectanglesPerPage.forEach((page, rectangle2D) -> {
            Rectangle2D paddedRectangle2D = rectangle2D;
            if (entry.getType() == NodeType.SECTION) {
                paddedRectangle2D = pad(rectangle2D, 10, 10);
            }
            drawRectangle2DList(document, page.getNumber(), List.of(paddedRectangle2D), options);
            drawText(buildString(entry), document, new Point2D.Double(paddedRectangle2D.getMinX(), paddedRectangle2D.getMaxY() + 2), page.getNumber(), options);
        });
    }


    private static Rectangle2D pad(Rectangle2D rectangle2D, int deltaX, int deltaY) {

        return new Rectangle2D.Double(rectangle2D.getMinX() - deltaX, rectangle2D.getMinY() - deltaY, rectangle2D.getWidth() + 2 * deltaX, rectangle2D.getHeight() + 2 * deltaY);
    }


    private String buildString(DocumentTree.Entry entry) {

        return entry.getNode().getNumberOnPage() + ": " + entry.getTreeId() + ": " + entry.getType().toString();
    }


    @SneakyThrows
    public static void drawLine2DList(PDDocument pdDocument, int pageNumber, List<? extends Line2D> line2DS, DrawingOptions options) {

        var pdPage = pdDocument.getPage(pageNumber - 1);
        var contentStream = new PDPageContentStream(pdDocument, pdPage, PDPageContentStream.AppendMode.APPEND, true);

        contentStream.setStrokingColor(options.getStrokeColor());
        contentStream.setNonStrokingColor(options.getFillColor());
        contentStream.setLineWidth(options.getStrokeWidth());

        for (var line2D : line2DS) {
            contentStream.moveTo((float) line2D.getX1(), (float) line2D.getY1());
            contentStream.lineTo((float) line2D.getX2(), (float) line2D.getY2());

            if (options.isStroke() && options.isFill()) {
                contentStream.fillAndStroke();
            } else if (options.isStroke()) {
                contentStream.stroke();
            } else if (options.isFill()) {
                contentStream.fill();
            }
        }
        contentStream.close();
    }

}
