package com.knecon.fforesight.service.layoutparser.processor.model.outline;

import java.awt.geom.Point2D;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Optional;

import org.apache.pdfbox.cos.COSArray;
import org.apache.pdfbox.cos.COSBase;
import org.apache.pdfbox.cos.COSDictionary;
import org.apache.pdfbox.cos.COSName;
import org.apache.pdfbox.cos.COSString;
import org.apache.pdfbox.pdmodel.PDDestinationNameTreeNode;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDDocumentNameDictionary;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.interactive.action.PDAction;
import org.apache.pdfbox.pdmodel.interactive.documentnavigation.destination.PDDestination;
import org.apache.pdfbox.pdmodel.interactive.documentnavigation.destination.PDPageFitHeightDestination;
import org.apache.pdfbox.pdmodel.interactive.documentnavigation.destination.PDPageFitRectangleDestination;
import org.apache.pdfbox.pdmodel.interactive.documentnavigation.destination.PDPageFitWidthDestination;
import org.apache.pdfbox.pdmodel.interactive.documentnavigation.destination.PDPageXYZDestination;
import org.apache.pdfbox.pdmodel.interactive.documentnavigation.outline.PDDocumentOutline;
import org.apache.pdfbox.pdmodel.interactive.documentnavigation.outline.PDOutlineItem;
import org.springframework.stereotype.Service;

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

@Service
@Slf4j
public class OutlineExtractorService {

    private static final String PDDESTINATION_TYPE_FIT = "Fit";
    private static final String PDDESTINATION_TYPE_FIT_B = "FitB";
    private static final String PDDESTINATION_TYPE_FIT_H = "FitH";
    private static final String PDDESTINATION_TYPE_FIT_V = "FitV";
    private static final String PDDESTINATION_TYPE_FIT_R = "FitR";
    private static final String PDDESTINATION_TYPE_FIT_BH = "FitBH";
    private static final String PDDESTINATION_TYPE_FIT_BV = "FitBV";
    private static final String PDDESTINATION_TYPE_XYZ = "XYZ";


    @SneakyThrows
    public OutlineObjectTree getOutlineObjectTree(PDDocument document) {

        PDDocumentOutline documentOutline = document.getDocumentCatalog().getDocumentOutline();

        List<OutlineObjectTreeNode> rootNodes = new ArrayList<>();
        if (documentOutline != null) {
            for (PDOutlineItem child : documentOutline.children()) {
                Optional<OutlineObjectTreeNode> outlineObjectWithChildren = createOutlineObjectWithChildren(child, document, 1);
                outlineObjectWithChildren.ifPresent(rootNodes::add);
            }
        }

        return new OutlineObjectTree(rootNodes);
    }


    @SneakyThrows
    private Optional<OutlineObjectTreeNode> createOutlineObjectWithChildren(PDOutlineItem item, PDDocument document, int depth) {

        Optional<OutlineObjectTreeNode> outlineObject = createOutlineObject(item, document, depth);
        if (outlineObject.isPresent()) {
            for (var child : item.children()) {
                Optional<OutlineObjectTreeNode> outlineObjectWithChildren = createOutlineObjectWithChildren(child, document, depth + 1);
                outlineObjectWithChildren.ifPresent(outlineObjectTreeNode -> outlineObject.get().addChild(outlineObjectTreeNode));
            }
        }

        return outlineObject;
    }


    // if the structure elements are processed beforehand, another case can be handled here as well:
    // outline objects can reference structure elements (see pdf documentation)
    @SneakyThrows
    private Optional<OutlineObjectTreeNode> createOutlineObject(PDOutlineItem item, PDDocument document, int depth) {

        String title = item.getTitle();

        PDPage page = item.findDestinationPage(document);
        if (page == null) {
            return Optional.empty();
        }
        int pageNumber = document.getPages().indexOf(page);

        Optional<Point2D> outlinePosition = Optional.empty();

        try {
            PDDocumentNameDictionary names = document.getDocumentCatalog().getNames();
            PDDestinationNameTreeNode destinations = null;
            if (names != null) {
                destinations = names.getDests();
            }

            PDDestination destination = item.getDestination();
            if (destination != null) {
                outlinePosition = getLocationFromCOSBase(destinations, destination.getCOSObject());
            }

            if (outlinePosition.isEmpty()) {

                PDAction action = item.getAction();
                if (action != null) {
                    outlinePosition = extractOutlineLocationGoTo(destinations, action.getCOSObject());
                }

            }

        } catch (Exception e) {
            log.info(String.format("Error occurred during position resolution for outline item on page %s with title %s: " + e, pageNumber, title));
        }

        return Optional.of(new OutlineObjectTreeNode(new OutlineObject(title, pageNumber, outlinePosition.orElse(new Point2D.Float(0, 0)), depth)));

    }


    @SneakyThrows
    private static Optional<Point2D> extractOutlineLocationGoTo(PDDestinationNameTreeNode destinations, COSDictionary cosDictionary) {

        if (isGoToAction(cosDictionary)) {
            COSBase cosBase = cosDictionary.getItem(COSName.D);
            return getLocationFromCOSBase(destinations, cosBase);
        }

        return Optional.empty();
    }


    private static Optional<Point2D> getLocationFromCOSBase(PDDestinationNameTreeNode destinations, COSBase cosBase) throws IOException {

        if (cosBase != null) {
            if (cosBase instanceof COSArray cosArray) {
                return getLocationFromCosArray(cosArray);
            }

            if (cosBase instanceof COSString cosString) {
                String destinationName = cosString.getString();
                COSArray cosArray = destinations.getValue(destinationName).getCOSObject();
                return getLocationFromCosArray(cosArray);
            }

        }
        return Optional.empty();
    }


    private static Optional<Point2D> getLocationFromCosArray(COSArray cosArray) {

        boolean located = false;
        float x = 0;
        float y = 0;

        try {

            PDDestination destination = PDDestination.create(cosArray);
            COSName type = (COSName) cosArray.getObject(1);
            String typeString = type.getName();

            switch (typeString) {
                case PDDESTINATION_TYPE_FIT_V:
                case PDDESTINATION_TYPE_FIT_BV:
                    PDPageFitHeightDestination fitHeightDestination = (PDPageFitHeightDestination) destination;
                    x = fitHeightDestination.getLeft();
                    located = true;
                    break;
                case PDDESTINATION_TYPE_FIT_R:
                    PDPageFitRectangleDestination fitRectangleDestination = (PDPageFitRectangleDestination) destination;
                    x = fitRectangleDestination.getLeft();
                    y = fitRectangleDestination.getTop();
                    located = true;
                    break;
                case PDDESTINATION_TYPE_FIT_H:
                case PDDESTINATION_TYPE_FIT_BH:
                    PDPageFitWidthDestination fitWidthDestination = (PDPageFitWidthDestination) destination;
                    y = fitWidthDestination.getTop();
                    located = true;
                    break;
                case PDDESTINATION_TYPE_XYZ:
                    PDPageXYZDestination xyzDestination = (PDPageXYZDestination) destination;
                    x = xyzDestination.getLeft();
                    y = xyzDestination.getTop();
                    located = true;
                    break;
                case PDDESTINATION_TYPE_FIT:
                case PDDESTINATION_TYPE_FIT_B:
                default:
            }

        } catch (IOException e) {
            throw new RuntimeException(e);
        }

        return located ? Optional.of(new Point2D.Float(x, y)) : Optional.empty();

    }


    private static boolean isGoToAction(COSDictionary cosDictionary) {

        return cosDictionary.getNameAsString("S").toLowerCase(Locale.ROOT).equals("goto");
    }

}
