package com.knecon.fforesight.service.layoutparser.processor.services.graphics;

import java.awt.Rectangle;
import java.awt.color.CMMException;
import java.awt.geom.GeneralPath;
import java.awt.geom.Point2D;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;

import org.apache.pdfbox.contentstream.PDFGraphicsStreamEngine;
import org.apache.pdfbox.contentstream.PDFStreamEngine;
import org.apache.pdfbox.contentstream.operator.Operator;
import org.apache.pdfbox.contentstream.operator.OperatorProcessor;
import org.apache.pdfbox.cos.COSBase;
import org.apache.pdfbox.cos.COSName;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.graphics.color.PDColor;
import org.apache.pdfbox.pdmodel.graphics.image.PDImage;

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

@Slf4j
public class GraphicBBDetector extends PDFGraphicsStreamEngine {

    private int clipWindingRule = -1;
    private final GeneralPath linePath = new GeneralPath();
    private final List<Rectangle> bounds = new ArrayList<>();
    private final PDColor emptyPattern = new PDColor(new float[3], null);


    public GraphicBBDetector(PDPage page, boolean ignoreWhite) {

        super(page);

        if (!ignoreWhite) {
            addOperator(new NullOp("d", this));
            addOperator(new NullOp("k", this));
            addOperator(new NullOp("K", this));
            addOperator(new NullOp("g", this));
            addOperator(new NullOp("G", this));
            addOperator(new NullOp("CS", this));
            addOperator(new NullOp("cs", this));
            addOperator(new NullOp("RG", this));
            addOperator(new NullOp("rg", this));
            addOperator(new NullOp("sc", this));
            addOperator(new NullOp("SC", this));
            addOperator(new NullOp("scn", this));
            addOperator(new NullOp("SCN", this));
        }

        // Ignore text and font ops:
        addOperator(new NullOp("Tf", this));
        addOperator(new NullOp("Tj", this));
        addOperator(new NullOp("TJ", this));
        addOperator(new NullOp("T*", this));
        addOperator(new NullOp("'", this));
        addOperator(new NullOp("\"", this));
    }


    public List<Box> findGraphicBB() throws IOException {

        processPage(getPage());
        return bounds.stream().map(r -> new Box(r.x, r.y, r.x + r.width, r.y + r.height)).filter(box -> box.area() > 0).collect(Collectors.toList());
    }


    @Override
    public void appendRectangle(Point2D p0, Point2D p1, Point2D p2, Point2D p3) {

        linePath.moveTo((float) p0.getX(), (float) p0.getY());
        linePath.lineTo((float) p1.getX(), (float) p1.getY());
        linePath.lineTo((float) p2.getX(), (float) p2.getY());
        linePath.lineTo((float) p3.getX(), (float) p3.getY());
        linePath.closePath();
    }


    @Override
    public void drawImage(PDImage pdImage) {
        // Do nothing
    }


    @Override
    public void clip(int windingRule) {

        clipWindingRule = windingRule;
    }


    @Override
    public void moveTo(float x, float y) {

        linePath.moveTo(x, y);
    }


    @Override
    public void lineTo(float x, float y) {

        linePath.lineTo(x, y);
    }


    @Override
    public void curveTo(float x1, float y1, float x2, float y2, float x3, float y3) {

        linePath.curveTo(x1, y1, x2, y2, x3, y3);
    }


    @Override
    public Point2D getCurrentPoint() {

        return linePath.getCurrentPoint();
    }


    @Override
    public void closePath() {

        linePath.closePath();
    }


    @Override
    public void endPath() {

        if (clipWindingRule != -1) {
            linePath.setWindingRule(clipWindingRule);
            getGraphicsState().intersectClippingPath(linePath);
            clipWindingRule = -1;
        }
        linePath.reset();
    }


    @Override
    public void strokePath() throws IOException {

        addLinePath(true, false);
        linePath.reset();
    }


    @Override
    public void fillPath(int windingRule) throws IOException {

        linePath.setWindingRule(windingRule);
        addLinePath(false, true);
        linePath.reset();
    }


    @Override
    public void fillAndStrokePath(int windingRule) throws IOException {

        linePath.setWindingRule(windingRule);
        addLinePath(true, true);
        linePath.reset();
    }


    @Override
    public void shadingFill(COSName shadingName) {

        var newBound = getGraphicsState().getCurrentClippingPath().getBounds();
        if (newBound.getWidth() > 0 && newBound.getHeight() > 0) {
            bounds.add(newBound);
        }
    }


    private void addLinePath(boolean stroke, boolean fill) throws IOException {

        var newBound = getGraphicsState().getCurrentClippingPath().getBounds().intersection(linePath.getBounds());
        if (newBound.getWidth() > 0 && newBound.getHeight() > 0) {

            if (stroke && !getGraphicsState().getStrokingColor().isPattern() && isBlack(getGraphicsState().getStrokingColor()) || //
                    !stroke && !getGraphicsState().getNonStrokingColor().isPattern() && isBlack(getGraphicsState().getNonStrokingColor())) {
                bounds.add(newBound);
            }

//            var skipWhiteGraphic = ignoreWhite && (!stroke || isWhite(getGraphicsState().getStrokingColor())) && (!fill || isWhite(getGraphicsState().getNonStrokingColor()));
//            if (!skipWhiteGraphic) {
//                bounds.add(newBound);
//            }
        }
    }


    @SneakyThrows
    private boolean isBlack(PDColor color) {

        try {
            return color.toRGB() == 0;
        } catch (CMMException e) {
            // see https://github.com/haraldk/TwelveMonkeys/issues/124 or https://issues.apache.org/jira/browse/PDFBOX-3531
            // This is a quick and dirt hack
            // Happens for file 216.pdf
            log.debug(e.getMessage());
            var result = true;
            for (var component : color.getComponents()) {
                result = result && component == 0;
            }
            return result;
        }
    }


    private boolean isWhite(PDColor color) throws IOException {

        return !color.isPattern() && color.toRGB() == 16777215 || color.equals(emptyPattern);
    }


    private final class NullOp extends OperatorProcessor {

        private final String name;


        private NullOp(String name, PDFStreamEngine context) {

            super(context);
            this.name = name;
        }


        @Override
        public void process(Operator operator, List<COSBase> operands) {
            // Do nothing.
        }


        @Override
        public String getName() {

            return name;
        }

    }

}
