package com.knecon.fforesight.service.viewerdoc.service;

import java.awt.geom.AffineTransform;
import java.awt.geom.Line2D;
import java.awt.geom.Rectangle2D;
import java.util.List;
import java.util.Map;

import org.apache.pdfbox.pdmodel.graphics.state.RenderingMode;

import com.knecon.fforesight.service.viewerdoc.LayerIdentifier;
import com.knecon.fforesight.service.viewerdoc.layers.LayerGroup;
import com.knecon.fforesight.service.viewerdoc.model.ColoredLine;
import com.knecon.fforesight.service.viewerdoc.model.ColoredRectangle;
import com.knecon.fforesight.service.viewerdoc.model.EmbeddableFont;
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 com.knecon.fforesight.service.viewerdoc.model.VisualizationsOnPage;
import com.pdftron.common.Matrix2D;
import com.pdftron.common.PDFNetException;
import com.pdftron.pdf.ColorPt;
import com.pdftron.pdf.ColorSpace;
import com.pdftron.pdf.Element;
import com.pdftron.pdf.ElementBuilder;
import com.pdftron.pdf.ElementReader;
import com.pdftron.pdf.ElementWriter;
import com.pdftron.pdf.Font;
import com.pdftron.pdf.GState;
import com.pdftron.pdf.Page;
import com.pdftron.pdf.ocg.Group;

import lombok.AccessLevel;
import lombok.Builder;
import lombok.SneakyThrows;
import lombok.experimental.FieldDefaults;

@Builder
@FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE)
public class VisualizationWriter {

    ElementWriter writer;
    ElementBuilder builder;
    ElementReader reader;
    List<LayerGroup> layerGroups;
    Map<LayerIdentifier, Group> groupMap;
    Map<EmbeddableFont, Font> fontMap;


    @SneakyThrows
    public void drawVisualizationsOnPage(int pageNumber, Page page) {

        begin(page);

        AffineTransform textDeRotationMatrix = getTextDeRotationTransform(page);
        AffineTransform pageTransformation = getTextDeRotationTransform(page);

        for (LayerGroup layerGroup : layerGroups) {

            Element markedContentStart = builder.createMarkedContentBeginInlineProperties(layerGroup.getGroupIdentifier().markedContentName());
            writer.writeElement(markedContentStart);

            if (layerGroup.isOptionalContent()) {
                Element ocgStart = builder.createMarkedContentBegin("OC", groupMap.get(layerGroup.getGroupIdentifier()).getSDFObj());
                writer.writeElement(ocgStart);
            }

            Element escape = builder.createGroupBegin();
            writer.writeElement(escape);

            writeVisualizations(pageNumber, layerGroup, textDeRotationMatrix);

            Element escapeEnd = builder.createGroupEnd();
            writer.writeElement(escapeEnd);

            if (layerGroup.isOptionalContent()) {
                Element ocgEnd2 = builder.createMarkedContentEnd();
                writer.writeElement(ocgEnd2);
            }

            Element markedContentEnd = builder.createMarkedContentEnd();
            writer.writeElement(markedContentEnd);
        }

        end();

    }


    private void writeVisualizations(int pageNumber, LayerGroup layerGroup, AffineTransform textDeRotationMatrix) throws PDFNetException {

        for (Visualizations visualization : layerGroup.getVisualizations()) {

            VisualizationsOnPage visualizationsOnPage = visualization.getVisualizationsOnPages().get(pageNumber);

            if (visualizationsOnPage == null || visualizationsOnPage.isEmpty()) {
                continue;
            }

            Element markedContentStart = builder.createMarkedContentBeginInlineProperties(visualization.getLayer().markedContentName());
            writer.writeElement(markedContentStart);

            if (layerGroup.subLayersAreOptionalContent()) {
                Element ocgStart = builder.createMarkedContentBegin("OC", groupMap.get(visualization.getLayer()).getSDFObj());
                writer.writeElement(ocgStart);
            }

            Element escape = builder.createGroupBegin();
            writer.writeElement(escape);


            writeVisualization(visualizationsOnPage, textDeRotationMatrix);

            Element escapeEnd = builder.createGroupEnd();
            writer.writeElement(escapeEnd);

            if (layerGroup.subLayersAreOptionalContent()) {
                Element ocgEnd = builder.createMarkedContentEnd();
                writer.writeElement(ocgEnd);
            }

            Element markedContentEnd = builder.createMarkedContentEnd();
            writer.writeElement(markedContentEnd);

        }
    }


    private void end() throws PDFNetException {

        writer.end();
    }


    private void begin(Page page) throws PDFNetException {

        writer.begin(page, ElementWriter.e_overlay, false, true, page.getResourceDict());
    }


    @SneakyThrows
    private void writeVisualization(VisualizationsOnPage visualizationsOnPage, AffineTransform textDeRotationMatrix) {

        if (visualizationsOnPage.isMakePathsInvisible()) {
            Element rect = builder.createRect(0, 0, 0, 0);
            rect.setPathClip(true);
            writer.writeElement(rect);
        }

        for (ColoredLine coloredLine : visualizationsOnPage.getColoredLines()) {

            drawColoredLine(coloredLine);
        }

        for (ColoredRectangle coloredRectangle : visualizationsOnPage.getColoredRectangles()) {

            drawColoredRectangle(coloredRectangle);
        }

        for (FilledRectangle filledRectangle : visualizationsOnPage.getFilledRectangles()) {

            drawFilledRectangle(filledRectangle);
        }

        for (PlacedText placedText : visualizationsOnPage.getPlacedTexts()) {

            writePlacedText(textDeRotationMatrix, placedText);
        }

    }


    private void writePlacedText(AffineTransform textDeRotationMatrix, PlacedText placedText) throws PDFNetException {

        float[] rgbComponents = placedText.color().getRGBColorComponents(null);
        Font font = fontMap.get(placedText.font());
        Element text = builder.createTextBegin(font, placedText.fontSize());
        text.getGState().setFillColorSpace(ColorSpace.createDeviceRGB());
        try (ColorPt color = new ColorPt(rgbComponents[0], rgbComponents[1], rgbComponents[2])) {
            text.getGState().setFillColor(color);
        }

        try (Matrix2D textMatrix = getTextMatrix(placedText, textDeRotationMatrix)) {
            text.setTextMatrix(textMatrix);
        }

        text.getGState()
                .setTextRenderMode(placedText.renderingMode()
                                           .map(VisualizationWriter::resolveTextRenderMode).orElse(GState.e_fill_text));
        writer.writeElement(text);

        text = switch (font.getType()) {
            case Font.e_Type0, Font.e_CIDType0, Font.e_TrueType, Font.e_CIDType2 -> builder.createUnicodeTextRun(placedText.text());
            case Font.e_Type1 -> builder.createTextRun(placedText.text());
            default -> throw new IllegalStateException("Unexpected value: " + font.getType());
        };

        writer.writeElement(text);
        text = builder.createTextEnd();
        writer.writeElement(text);

    }


    private static int resolveTextRenderMode(RenderingMode renderingMode) {

        return switch (renderingMode) {
            case FILL -> GState.e_fill_text;
            case STROKE -> GState.e_stroke_text;
            case FILL_STROKE -> GState.e_fill_stroke_text;
            case NEITHER -> GState.e_invisible_text;
            case FILL_CLIP -> GState.e_fill_clip_text;
            case STROKE_CLIP -> GState.e_stroke_clip_text;
            case FILL_STROKE_CLIP -> GState.e_fill_stroke_clip_text;
            case NEITHER_CLIP -> GState.e_clip_text;
        };
    }


    private void drawFilledRectangle(FilledRectangle filledRectangle) throws PDFNetException {

        float[] rgbComponents = filledRectangle.color().getRGBColorComponents(null);
        Rectangle2D r = filledRectangle.rectangle2D();

        Element rect = builder.createRect(r.getX(), r.getY(), r.getWidth(), r.getHeight());

        rect.setPathFill(true);
        rect.setPathStroke(false);
        rect.getGState().setFillColorSpace(ColorSpace.createDeviceRGB());
        rect.getGState().setFillOpacity(filledRectangle.alpha());

        try (ColorPt color = new ColorPt(rgbComponents[0], rgbComponents[1], rgbComponents[2])) {
            rect.getGState().setFillColor(color);
        }

        writer.writeElement(rect);
    }


    private void drawColoredRectangle(ColoredRectangle coloredRectangle) throws PDFNetException {

        float[] rgbComponents = coloredRectangle.color().getRGBColorComponents(null);
        Rectangle2D r = coloredRectangle.rectangle2D();

        Element rect = builder.createRect(r.getX(), r.getY(), r.getWidth(), r.getHeight());

        rect.setPathStroke(true);
        rect.setPathFill(false);
        rect.getGState().setStrokeColorSpace(ColorSpace.createDeviceRGB());
        rect.getGState().setLineWidth(coloredRectangle.lineWidth());

        try (ColorPt color = new ColorPt(rgbComponents[0], rgbComponents[1], rgbComponents[2])) {
            rect.getGState().setStrokeColor(color);
        }

        writer.writeElement(rect);
    }


    private void drawColoredLine(ColoredLine coloredLine) throws PDFNetException {

        float[] rgbComponents = coloredLine.color().getRGBColorComponents(null);
        Line2D l = coloredLine.line();

        builder.pathBegin();
        builder.moveTo(l.getX1(), l.getY1());
        builder.lineTo(l.getX2(), l.getY2());
        Element line = builder.pathEnd();

        line.setPathStroke(true);
        line.setPathFill(false);
        line.getGState().setLineWidth(coloredLine.lineWidth());
        line.getGState().setStrokeColorSpace(ColorSpace.createDeviceRGB());

        try (ColorPt color = new ColorPt(rgbComponents[0], rgbComponents[1], rgbComponents[2])) {
            line.getGState().setStrokeColor(color);
        }
        writer.writeElement(line);
    }


    @SneakyThrows
    private static Matrix2D getTextMatrix(PlacedText placedText, AffineTransform textDeRotationMatrix) {

        Matrix2D textMatrix;
        if (placedText.textMatrix().isEmpty()) {
            textMatrix = toMatrix2D(textDeRotationMatrix, placedText.lineStart().getX(), placedText.lineStart().getY());
        } else {
            var matrix = placedText.textMatrix().get();
            textMatrix = toMatrix2D(matrix);
        }
        return textMatrix;
    }


    private static Matrix2D toMatrix2D(AffineTransform matrix) throws PDFNetException {

        return new Matrix2D(matrix.getScaleX(), matrix.getShearY(), matrix.getShearX(), matrix.getScaleY(), matrix.getTranslateX(), matrix.getTranslateY());
    }


    private static Matrix2D toMatrix2D(AffineTransform textDeRotationMatrix, double translateX, double translateY) throws PDFNetException {

        return new Matrix2D(textDeRotationMatrix.getScaleX(),
                            textDeRotationMatrix.getShearY(),
                            textDeRotationMatrix.getShearX(),
                            textDeRotationMatrix.getScaleY(),
                            translateX,
                            translateY);
    }


    @SneakyThrows
    private static AffineTransform getTextDeRotationTransform(Page page) {

        return AffineTransform.getQuadrantRotateInstance(switch (page.getRotation()) {
            case 90 -> 3;
            case 180 -> 2;
            case 270 -> 1;
            default -> 0;
        });
    }

}
