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

import java.util.LinkedList;
import java.util.List;
import java.util.Optional;

import org.apache.pdfbox.contentstream.PDContentStream;
import org.apache.pdfbox.contentstream.operator.OperatorName;
import org.apache.pdfbox.cos.COSName;
import org.apache.pdfbox.pdmodel.PDPage;

import com.knecon.fforesight.service.viewerdoc.ContentStreams;
import com.knecon.fforesight.service.viewerdoc.model.OperatorWithArguments;
import com.knecon.fforesight.service.viewerdoc.pdf.ClassifiedContentStream;
import com.knecon.fforesight.service.viewerdoc.pdf.SinglePDContentStream;

import lombok.SneakyThrows;
import lombok.experimental.UtilityClass;

@UtilityClass
public class ContentStreamClassifier {

    public List<ClassifiedContentStream> getClassifiedContentStreams(PDPage page) {

        List<SinglePDContentStream> streams = new LinkedList<>();
        page.getContentStreams().forEachRemaining(stream -> streams.add(new SinglePDContentStream(stream)));
        return ContentStreamClassifier.classifySingleContentStreams(page, streams);
    }


    public List<ClassifiedContentStream> classifySingleContentStreams(PDPage page, List<SinglePDContentStream> streams) {

        return streams.stream().map(singlePDContentStream -> classifySingleContentStream(page, singlePDContentStream)).toList();
    }


    private ClassifiedContentStream classifySingleContentStream(PDPage page, SinglePDContentStream singlePDContentStream) {

        ContentStreams.Identifier classification = classifyContentStream(singlePDContentStream, page);
        return new ClassifiedContentStream(singlePDContentStream, classification);
    }


    /**
     * We assume all of our layers are written escaped, so only unknown content streams need to be escaped.
     *
     * @param classifiers List of all content streams of a page with their classification
     * @return false, if any content stream with classification other is not prefixed with an ESCAPE_START and suffixed with an ESCAPE_END
     */
    public boolean areAllContentStreamsEscaped(List<ClassifiedContentStream> classifiers) {

        int escapeDepth = 0;
        for (ClassifiedContentStream classifier : classifiers) {
            if (classifier.classification().equals(ContentStreams.OTHER) && escapeDepth == 0) {
                return false;
            }
            if (classifier.classification().equals(ContentStreams.ESCAPE_START)) {
                escapeDepth++;
            }
            if (classifier.classification().equals(ContentStreams.ESCAPE_END)) {
                escapeDepth--;
            }
        }
        return escapeDepth == 0;
    }


    @SneakyThrows
    public ContentStreams.Identifier classifyContentStream(PDContentStream contentStream, PDPage page) {

        List<OperatorWithArguments> operatorsWithArguments = ContentStreamUtility.parseLeadingOperators(contentStream, 2);
        if (operatorsWithArguments.isEmpty()) {
            return ContentStreams.OTHER;
        }
        OperatorWithArguments firstOperator = operatorsWithArguments.get(0);

        // If we wrap the content streams we append and prepend a content stream with exactly one operator "q" or "Q".
        if (operatorsWithArguments.size() == 1) {
            if (firstOperator.operator().getName().equals(OperatorName.SAVE)) {
                return ContentStreams.ESCAPE_START;
            }
            if (firstOperator.operator().getName().equals(OperatorName.RESTORE)) {
                return ContentStreams.ESCAPE_END;
            }
        }

        // In previous versions we did not set a marked content with an explicit name. Instead, we wrote an optional content group (OCG) with the name "Layout grid".
        // This OCG is then assigned a COSName by PDFBox. Usually its "oc1".
        // Thus, in order to find this name we need to look in the page resources to find the COSName assigned to the OCG.
        // This COSName can then be found as an argument for the first operator in the content stream.
        if (firstOperator.operator().getName().equals(OperatorName.BEGIN_MARKED_CONTENT_SEQ)) {
            Optional<COSName> layoutGridOCGName = ContentStreamUtility.findLayoutGridOCGName(page);
            if (layoutGridOCGName.isPresent()) {
                if (arumentsContainLayoutGridOCG(firstOperator, layoutGridOCGName.get())) {
                    return ContentStreams.KNECON_LAYOUT;
                }
            }
        }

        if (!firstOperator.operator().getName().equals(OperatorName.BEGIN_MARKED_CONTENT)) {
            return ContentStreams.OTHER;
        }

        Optional<COSName> firstCOSNameFromArguments = firstOperator.arguments().stream().filter(c -> c instanceof COSName).map(c -> (COSName) c).findFirst();

        if (firstCOSNameFromArguments.isEmpty()) {
            return ContentStreams.OTHER;
        }

        var cosName = firstCOSNameFromArguments.get();

        return ContentStreams.allContentStreams.stream().filter(identifier -> identifier.cosName().equals(cosName)).findAny().orElse(ContentStreams.OTHER);
    }


    private static boolean arumentsContainLayoutGridOCG(OperatorWithArguments operator, COSName layoutGridOCGName) {

        return operator.arguments().stream().filter(c -> c instanceof COSName).map(c -> (COSName) c).anyMatch(cosName -> cosName.equals(layoutGridOCGName));
    }

}
