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

import java.io.File;
import java.io.FileInputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;

import com.knecon.fforesight.service.viewerdoc.LayerIdentifier;
import com.knecon.fforesight.service.viewerdoc.layers.IdpLayerConfig;
import com.knecon.fforesight.service.viewerdoc.layers.LayerGroup;
import com.knecon.fforesight.service.viewerdoc.layers.LayoutDebugLayerConfig;
import com.knecon.fforesight.service.viewerdoc.layers.LayoutGridLayerConfig;
import com.knecon.fforesight.service.viewerdoc.layers.OcrDebugLayerConfig;
import com.knecon.fforesight.service.viewerdoc.model.EmbeddableFont;
import com.knecon.fforesight.service.viewerdoc.model.Outline;
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.pdf.ElementBuilder;
import com.pdftron.pdf.ElementReader;
import com.pdftron.pdf.ElementWriter;
import com.pdftron.pdf.Font;
import com.pdftron.pdf.PDFDoc;
import com.pdftron.pdf.PDFNet;
import com.pdftron.pdf.Page;
import com.pdftron.pdf.PageIterator;
import com.pdftron.pdf.ocg.Group;
import com.pdftron.sdf.SDFDoc;

import io.micrometer.observation.ObservationRegistry;
import io.micrometer.observation.annotation.Observed;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@RequiredArgsConstructor
public class PDFTronViewerDocumentService {

    private final ObservationRegistry registry;

    public static final List<LayerGroup> ALL_LAYERS_WITH_OPTIONAL_CONTENT = List.of(LayoutGridLayerConfig.CONFIG_INSTANCE,
                                                                                    OcrDebugLayerConfig.CONFIG_INSTANCE,
                                                                                    LayoutDebugLayerConfig.CONFIG_INSTANCE,
                                                                                    IdpLayerConfig.CONFIG_INSTANCE);


    @SneakyThrows
    @Observed(name = "PDFTronViewerDocumentService", contextualName = "add-visualizations")
    public void addLayerGroups(File originFile, File destinationFile, List<LayerGroup> layerGroups, String layoutParserVersion, String layoutParsingType, Outline outline) {

        synchronized (PDFNet.class) { // synchronized with class, to ensure multiple instances are also synchronized

            // originFile and destinationFile might be the same, so we use a temp file.
            // Otherwise, saving the document might corrupt the file
            Path tmpFile = Files.createTempFile("tmpViewerDocument", ".pdf");
            Files.copy(originFile.toPath(), tmpFile, StandardCopyOption.REPLACE_EXISTING);

            try (PDFDoc pdfDoc = loadPdfDoc(tmpFile);//
                 ElementWriter pageWriter = new ElementWriter();//
                 ElementReader reader = new ElementReader();//
                 ElementBuilder builder = new ElementBuilder()//
            ) {
                enrichObservation(registry,
                                  pdfDoc.getPageCount(),
                                  layerGroups.stream()
                                          .map(LayerGroup::getVisualizations)
                                          .flatMap(Collection::stream)
                                          .map(Visualizations::getLayer)
                                          .toList());

                Map<LayerIdentifier, Group> groupMap = PdftronLayerUtility.addLayersToDocument(layerGroups, pdfDoc);

                Map<EmbeddableFont, Font> fontMap = buildFontMap(layerGroups, pdfDoc);

                Set<String> markedContentToDraw = mapMarkedContentNames(layerGroups);

                PageContentCleaner pageContentCleaner = PageContentCleaner.builder()
                        .writer(pageWriter)
                        .reader(reader)
                        .elementBuilder(builder)
                        .markedContentToRemove(markedContentToDraw)
                        .build();

                VisualizationWriter visualizationWriter = VisualizationWriter.builder()
                        .writer(pageWriter)
                        .builder(builder)
                        .groupMap(groupMap)
                        .layerGroups(layerGroups)
                        .fontMap(fontMap)
                        .build();

                boolean isCurrentVersion = ViewerDocVersioningUtility.docIsCurrentVersion(pdfDoc);

                int pageNumber = 1;
                try (PageIterator iterator = pdfDoc.getPageIterator()) {
                    while (iterator.hasNext()) {

                        Page page = iterator.next();

                        if (isCurrentVersion) {
                            pageContentCleaner.removeMarkedContent(page);
                        }

                        visualizationWriter.drawVisualizationsOnPage(pageNumber, page);
                        pageNumber++;
                    }
                }

//                OutlineUtility.addOutline(pdfDoc, outline);

                ViewerDocVersioningUtility.setVersionInDocument(pdfDoc, layoutParserVersion, layoutParsingType);

                saveDocument(pdfDoc, destinationFile);
            } finally {
                assert !tmpFile.toFile().exists() || tmpFile.toFile().delete();
            }
        }
    }


    @SneakyThrows
    @Observed(name = "PDFTronViewerDocumentService", contextualName = "add-visualizations")
    public void addLayerGroups(File originFile, File destinationFile, List<LayerGroup> layerGroups, String layoutParserVersion, String layoutParsingType) {

        addLayerGroups(originFile, destinationFile, layerGroups, layoutParserVersion, layoutParsingType, new Outline());
    }


    private static Set<String> mapMarkedContentNames(List<LayerGroup> layerGroups) {

        return layerGroups.stream()
                .map(LayerGroup::getVisualizations)
                .flatMap(Collection::stream)
                .map(Visualizations::getLayer)
                .map(LayerIdentifier::markedContentName)
                .collect(Collectors.toSet());
    }


    private static Map<EmbeddableFont, Font> buildFontMap(List<LayerGroup> layerGroups, PDFDoc pdfDoc) {

        return layerGroups.stream()
                .map(LayerGroup::getVisualizations)
                .flatMap(Collection::stream)
                .map(Visualizations::getVisualizationsOnPages)
                .map(Map::values)
                .flatMap(Collection::stream)
                .map(VisualizationsOnPage::getPlacedTexts)
                .flatMap(Collection::stream)
                .map(PlacedText::font)
                .distinct()
                .collect(Collectors.toMap(Function.identity(), font -> font.embed(pdfDoc)));

    }


    @SneakyThrows
    private void saveDocument(PDFDoc doc, File target) {

        doc.save(target.toString(), SDFDoc.SaveMode.REMOVE_UNUSED, null);
    }


    @SneakyThrows
    private static PDFDoc loadPdfDoc(Path tmpFile) {

        try (var in = new FileInputStream(tmpFile.toFile())) {
            return new PDFDoc(in);
        }
    }


    private void enrichObservation(ObservationRegistry registry, int numberOfPages, List<LayerIdentifier> layers) {

        if (registry == null || registry.getCurrentObservation() == null || registry.isNoop()) {
            return;
        }
        registry.getCurrentObservation().highCardinalityKeyValue("numberOfPages", String.valueOf(numberOfPages));
        for (int i = 0; i < layers.size(); i++) {
            LayerIdentifier layer = layers.get(i);

            registry.getCurrentObservation().highCardinalityKeyValue("layer_" + i, String.valueOf(layer.name()));
        }
    }

}
