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

import java.awt.color.CMMException;
import java.awt.geom.Point2D;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

import org.apache.pdfbox.contentstream.operator.Operator;
import org.apache.pdfbox.contentstream.operator.OperatorName;
import org.apache.pdfbox.contentstream.operator.color.SetNonStrokingColor;
import org.apache.pdfbox.contentstream.operator.color.SetNonStrokingColorN;
import org.apache.pdfbox.contentstream.operator.color.SetNonStrokingColorSpace;
import org.apache.pdfbox.contentstream.operator.color.SetNonStrokingDeviceCMYKColor;
import org.apache.pdfbox.contentstream.operator.color.SetNonStrokingDeviceGrayColor;
import org.apache.pdfbox.contentstream.operator.color.SetNonStrokingDeviceRGBColor;
import org.apache.pdfbox.contentstream.operator.color.SetStrokingColor;
import org.apache.pdfbox.contentstream.operator.color.SetStrokingColorN;
import org.apache.pdfbox.contentstream.operator.color.SetStrokingColorSpace;
import org.apache.pdfbox.contentstream.operator.color.SetStrokingDeviceCMYKColor;
import org.apache.pdfbox.contentstream.operator.color.SetStrokingDeviceGrayColor;
import org.apache.pdfbox.contentstream.operator.color.SetStrokingDeviceRGBColor;
import org.apache.pdfbox.contentstream.operator.markedcontent.BeginMarkedContentSequenceWithProperties;
import org.apache.pdfbox.contentstream.operator.markedcontent.EndMarkedContentSequence;
import org.apache.pdfbox.contentstream.operator.state.SetFlatness;
import org.apache.pdfbox.contentstream.operator.state.SetLineCapStyle;
import org.apache.pdfbox.contentstream.operator.state.SetLineDashPattern;
import org.apache.pdfbox.contentstream.operator.state.SetLineJoinStyle;
import org.apache.pdfbox.contentstream.operator.state.SetLineMiterLimit;
import org.apache.pdfbox.contentstream.operator.state.SetLineWidth;
import org.apache.pdfbox.contentstream.operator.state.SetRenderingIntent;
import org.apache.pdfbox.contentstream.operator.text.SetFontAndSize;
import org.apache.pdfbox.cos.COSBase;
import org.apache.pdfbox.cos.COSNumber;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.graphics.color.PDColor;
import org.apache.pdfbox.text.TextPosition;

import com.knecon.fforesight.service.layoutparser.processor.model.table.Ruling;
import com.knecon.fforesight.service.layoutparser.processor.model.text.RedTextPosition;
import com.knecon.fforesight.service.layoutparser.processor.model.text.TextPositionSequence;

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

@Getter
@Slf4j
public class PDFLinesTextStripper extends PDFTextStripper {

    private final List<TextPositionSequence> textPositionSequences = new ArrayList<>();
    private final List<Ruling> rulings = new ArrayList<>();
    private final List<Ruling> graphicsPath = new ArrayList<>();
    @Setter
    protected PDPage pdpage;

    private float path_x;
    private float path_y;

    @Setter
    private int pageNumber;


    public PDFLinesTextStripper() throws IOException {

        super();
        this.addOperator(new SetStrokingColorSpace(this));
        this.addOperator(new SetNonStrokingColorSpace(this));
        this.addOperator(new SetLineDashPattern(this));
        this.addOperator(new SetStrokingDeviceGrayColor(this));
        this.addOperator(new SetNonStrokingDeviceGrayColor(this));
        this.addOperator(new SetFlatness(this));
        this.addOperator(new SetLineJoinStyle(this));
        this.addOperator(new SetLineCapStyle(this));
        this.addOperator(new SetStrokingDeviceCMYKColor(this));
        this.addOperator(new SetNonStrokingDeviceCMYKColor(this));
        this.addOperator(new SetLineMiterLimit(this));
        this.addOperator(new SetStrokingDeviceRGBColor(this));
        this.addOperator(new SetNonStrokingDeviceRGBColor(this));
        this.addOperator(new SetRenderingIntent(this));
        this.addOperator(new SetStrokingColor(this));
        this.addOperator(new SetNonStrokingColor(this));
        this.addOperator(new SetStrokingColorN(this));
        this.addOperator(new SetNonStrokingColorN(this));
        this.addOperator(new SetFontAndSize(this));
        this.addOperator(new SetLineWidth(this));

        addOperator(new BeginMarkedContentSequenceWithProperties(this));
//        addOperator(new BeginMarkedContentSequence(this));
        addOperator(new EndMarkedContentSequence(this));
    }


    @Override
    protected void processOperator(Operator operator, List<COSBase> arguments) throws IOException {

        String operation = operator.getName();

        //move
        switch (operation) {

            case OperatorName.MOVE_TO:
                if (arguments.size() == 2) {
                    Point2D.Float pos = transformPosition(floatValue(arguments.get(0)), floatValue(arguments.get(1)));
                    path_x = (float) pos.getX();
                    path_y = (float) pos.getY();
                }
                break;

            //line
            case OperatorName.LINE_TO:
                if (arguments.size() == 2) {
                    Point2D.Float pos = transformPosition(floatValue(arguments.get(0)), floatValue(arguments.get(1)));

                    // The direction of vertical lines must always be from bottom to top for the table extraction algorithm.
                    if (pos.getY() > path_y) {
                        graphicsPath.add(new Ruling(new Point2D.Float(path_x, path_y), new Point2D.Float((float) pos.getX(), (float) pos.getY())));
                    } else {
                        graphicsPath.add(new Ruling(new Point2D.Float(path_x, (float) pos.getY()), new Point2D.Float((float) pos.getX(), path_y)));
                    }

                    path_x = (float) pos.getX();
                    path_y = (float) pos.getY();
                }
                break;

            //rectangle
            case OperatorName.APPEND_RECT:

                if (arguments.size() == 4) {
                    float x = floatValue(arguments.get(0));
                    float y = floatValue(arguments.get(1));
                    float width = floatValue(arguments.get(2));
                    float height = floatValue(arguments.get(3));

                    Point2D p1 = transformPosition(x, y);
                    Point2D p2 = transformPosition(x + width, y + height);

                    // Horizontal lines
                    graphicsPath.add(new Ruling(new Point2D.Float((float) p1.getX(), (float) p1.getY()), new Point2D.Float((float) p2.getX(), (float) p1.getY())));
                    graphicsPath.add(new Ruling(new Point2D.Float((float) p1.getX(), (float) p2.getY()), new Point2D.Float((float) p2.getX(), (float) p2.getY())));

                    // Vertical lines, direction must always be from bottom to top for the table extraction algorithm.
                    if (p2.getY() > p1.getY()) {
                        graphicsPath.add(new Ruling(new Point2D.Float((float) p2.getX(), (float) p1.getY()), new Point2D.Float((float) p2.getX(), (float) p2.getY())));
                    } else {
                        graphicsPath.add(new Ruling(new Point2D.Float((float) p2.getX(), (float) p2.getY()), new Point2D.Float((float) p2.getX(), (float) p1.getY())));
                    }
                    if (p2.getY() > p1.getY()) {
                        graphicsPath.add(new Ruling(new Point2D.Float((float) p1.getX(), (float) p1.getY()), new Point2D.Float((float) p1.getX(), (float) p2.getY())));
                    } else {
                        graphicsPath.add(new Ruling(new Point2D.Float((float) p1.getX(), (float) p2.getY()), new Point2D.Float((float) p1.getX(), (float) p1.getY())));
                    }
                }
                break;

            //fill
            case OperatorName.FILL_NON_ZERO:
            case OperatorName.LEGACY_FILL_NON_ZERO:
            case OperatorName.FILL_EVEN_ODD:
                addVisibleRulings(graphicsPath, false);
                graphicsPath.clear();
                break;

            //stroke
            case OperatorName.STROKE_PATH:
                addVisibleRulings(graphicsPath, true);
                graphicsPath.clear();
                break;

            //cancel path
            case OperatorName.ENDPATH:
                graphicsPath.clear();
                break;

        }

        super.processOperator(operator, arguments);
    }


    private float floatValue(COSBase value) {

        if (value instanceof COSNumber) {
            return ((COSNumber) value).floatValue();
        } else {
            return 0;
        }
    }


    private Point2D.Float transformPosition(float x, float y) {

        return super.transformedPoint(x, y);
    }


    private void addVisibleRulings(List<Ruling> path, boolean stroke) throws IOException {

        try {
            if (stroke && !getGraphicsState().getStrokingColor().isPattern() && isBlack(getGraphicsState().getStrokingColor()) || //
                    !stroke && !getGraphicsState().getNonStrokingColor().isPattern() && isBlack(getGraphicsState().getNonStrokingColor())) {
                rulings.addAll(path);
            }
        } catch (UnsupportedOperationException e) {
            log.debug("UnsupportedOperationException: " + getGraphicsState().getStrokingColor().getColorSpace().getName() + " or " + getGraphicsState().getNonStrokingColor()
                    .getColorSpace()
                    .getName() + " does not support toRGB");
        }
    }


    @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;
        }
    }


    @Override
    public void writeString(String text, List<TextPosition> textPositions, boolean isParagraphStart) throws IOException {

        int startIndex = 0;
        RedTextPosition previous = null;

        float direction = -1;
        for (int i = 0; i <= textPositions.size() - 1; i++) {

            if (direction == -1) {
                direction = textPositions.get(i).getDir();
            }

            if (!textPositionSequences.isEmpty()) {
                previous = textPositionSequences.get(textPositionSequences.size() - 1)
                        .getTextPositions()
                        .get(textPositionSequences.get(textPositionSequences.size() - 1).getTextPositions().size() - 1);
            }

            if (i == 0 && (textPositions.get(i).getUnicode().equals(" ") || textPositions.get(i).getUnicode().equals("\u00A0") || textPositions.get(i).getUnicode().equals("\t"))) {
                startIndex++;
                continue;
            }

            if (textPositions.get(i).getDir() != direction && startIndex != i) {
                List<TextPosition> sublist = textPositions.subList(startIndex, i);
                textPositionSequences.add(new TextPositionSequence(sublist, pageNumber, i == textPositions.size() - 1 && isParagraphStart));
                startIndex = i;
                direction = textPositions.get(i).getDir();
            }

            // Strange but sometimes this is happening, for example: Metolachlor2.pdf
            if (checkIfCurrentPositionIsToTheRightOfPreviousPosition(i, textPositions)) {
                List<TextPosition> sublist = textPositions.subList(startIndex, i);
                if (checkIfSequenceContainsOnlyWhitespaces(sublist)) {
                    textPositionSequences.add(new TextPositionSequence(sublist, pageNumber, i == textPositions.size() - 1 && isParagraphStart));
                }
                startIndex = i;
            }

            if (textPositions.get(i).getRotation() == 0 && i > 0 && textPositions.get(i).getX() > textPositions.get(i - 1).getEndX() + 1) {
                List<TextPosition> sublist = textPositions.subList(startIndex, i);
                if (checkIfSequenceContainsOnlyWhitespaces(sublist)) {
                    textPositionSequences.add(new TextPositionSequence(sublist, pageNumber, i == textPositions.size() - 1 && isParagraphStart));
                }
                startIndex = i;
            }

            if (i > 0 && (textPositions.get(i).getUnicode().equals(" ") || textPositions.get(i).getUnicode().equals("\u00A0") || textPositions.get(i)
                    .getUnicode()
                    .equals("\t")) && i <= textPositions.size() - 2) {
                List<TextPosition> sublist = textPositions.subList(startIndex, i);
                if (checkIfSequenceContainsOnlyWhitespaces(sublist)) {

                    // Remove false sequence ends (whitespaces)
                    if (checkIfGapSizeBetweenCharactersSmallerThanMaximum(previous, sublist, 0.01f)) {
                        for (TextPosition t : sublist) {
                            textPositionSequences.get(textPositionSequences.size() - 1).add(t);
                        }
                    } else {
                        textPositionSequences.add(new TextPositionSequence(sublist, pageNumber, i == textPositions.size() - 1 && isParagraphStart));
                    }
                }
                startIndex = i + 1;
            }
        }

        List<TextPosition> sublist = textPositions.subList(startIndex, textPositions.size());
        if (!sublist.isEmpty() && (sublist.get(sublist.size() - 1).getUnicode().equals(" ") || sublist.get(sublist.size() - 1)
                .getUnicode()
                .equals("\u00A0") || sublist.get(sublist.size() - 1).getUnicode().equals("\t"))) {
            sublist = sublist.subList(0, sublist.size() - 1);
        }

        if (!(sublist.isEmpty() || sublist.size() == 1 && (sublist.get(0).getUnicode().equals(" ") || sublist.get(0).getUnicode().equals("\u00A0") || sublist.get(0)
                .getUnicode()
                .equals("\t")))) {
            if (previous != null && sublist.get(0).getYDirAdj() == previous.getYDirAdj() && sublist.get(0)
                    .getXDirAdj() - (previous.getXDirAdj() + previous.getWidthDirAdj()) < 0.01) {
                for (TextPosition t : sublist) {
                    textPositionSequences.get(textPositionSequences.size() - 1).add(t);
                }
            } else {
                textPositionSequences.add(new TextPositionSequence(sublist, pageNumber, isParagraphStart));
            }
        }
        super.writeString(text);
    }


    public boolean checkIfCurrentPositionIsToTheRightOfPreviousPosition(int i, List<TextPosition> textPositions) {

        return i > 0 && textPositions.get(i).getXDirAdj() < textPositions.get(i - 1).getXDirAdj();
    }


    public boolean checkIfSequenceContainsOnlyWhitespaces(List<TextPosition> sublist) {

        return !(sublist.isEmpty() || sublist.size() == 1 && (sublist.get(0).getUnicode().equals(" ") || sublist.get(0).getUnicode().equals("\u00A0") || sublist.get(0)
                .getUnicode()
                .equals("\t")));
    }


    public boolean checkIfGapSizeBetweenCharactersSmallerThanMaximum(RedTextPosition previous, List<TextPosition> sublist, float maximumGapSize) {

        return previous != null && sublist.get(0).getYDirAdj() == previous.getYDirAdj() && sublist.get(0)
                .getXDirAdj() - (previous.getXDirAdj() + previous.getWidthDirAdj()) < maximumGapSize;
    }


    @Override
    public String getText(PDDocument doc) throws IOException {

        textPositionSequences.clear();
        rulings.clear();
        graphicsPath.clear();
        path_x = 0.0f;
        path_y = 0.0f;

        return super.getText(doc);

    }

}


