/*
 * Copyright (c) 1997-2025 IDRsolutions (https://www.idrsolutions.com)
 */
package org.jpedal.examples;

import org.jpedal.PdfDecoderServer;
import org.jpedal.exception.PdfException;
import org.jpedal.external.ExternalHandlers;
import org.jpedal.fonts.FontMappings;
import org.jpedal.objects.PdfFileInformation;
import org.jpedal.objects.PdfPageData;
import org.jpedal.objects.PdfResources;
import org.jpedal.objects.raw.PdfDictionary;
import org.jpedal.objects.raw.PdfObject;
import org.jpedal.objects.structuredtext.MarkedContentGenerator;
import org.jpedal.parser.image.ImageCommands;
import org.jpedal.render.DynamicVectorRenderer;

import java.util.HashMap;
import java.util.Map;

/**
 <h2>PdfUtilities</h2>
 <p>
 * This class provides a simple Java API to access general PDF features in JPEDAL via a simple API
 */
public class PdfUtilities extends BaseExample {

    /**
     <pre><code>
     *     PdfUtilities pdfUtils=new PdfUtilities("C:/pdfs/mypdf.pdf");
     *     // pdfUtils.setPassword("password");
     *     if (pdfUtils.openPDFFile()) {
     *         int commandsOnPage=pdfUtils.getCommandCountForPageStream();
     *     }
     *     pdfUtils.closePDFfile();
     </code></pre>
     *
     @param page pdf logical page
     *
     @return The count of Postscript commands from the page
     */
    public int getCommandCountForPageStream(final int page) {

        decode_pdf.setExtractionMode(0);
        decode_pdf.decodePage(page);

        return decode_pdf.getDynamicRenderer().getValue(DynamicVectorRenderer.TOKEN_NUMBER);

    }

    public enum PageUnits {
        Pixels,
        Inches,
        Centimetres
    }

    public enum PageSizeType {
        MediaBox,
        CropBox
    }

    private PdfFileInformation currentFileInformation;

    private boolean hasEmbeddedFonts;
    private boolean testIfFontsEmbedded;

    private PdfPageData currentPageData;




    public PdfUtilities(final String fileName) {
        this.fileName = fileName;

        init();

    }

    public PdfUtilities(final byte[] byteArray) {
        this.byteArray = byteArray;

        init();
    }

    /**
     @param password the USER or OWNER password for the PDF file
     */
    public void setPassword(final String password) {

        this.password = password;

    }

    private void init() {

        FontMappings.setFontReplacements();

        decode_pdf = new PdfDecoderServer(false)//false as no GUI display needed
    }

    @Override
    public boolean openPDFFile() throws PdfException {

        if (super.openPDFFile()) {
            /*get the Pdf file information object to extract info from*/
            currentFileInformation = decode_pdf.getFileInformationData();
            currentPageData = decode_pdf.getPdfPageData();
            return true;
        }

        return false;
    }

    /**
     * number of pages in PDF file (starting at 1)
     *
     @return page count
     */
    public int getPageCount() {

        checkFileOpened();

        return decode_pdf.getPageCount();
    }

    /**
     * get the PDF version number
     *
     @return PDF version
     */
    public String getPDFVersion() {

        checkFileOpened();

        return decode_pdf.getPDFVersion();
    }

    /**
     * get image data on pages in PDF file (starting at 1) (Method IS NOT THREADSAFE)
     *
     <pre><code>
     *     PdfUtilities pdfUtils=new PdfUtilities("C:/pdfs/mypdf.pdf");
     *     // pdfUtils.setPassword("password");
     *     if (pdfUtils.openPDFFile()) {
     *         String pageImages=pdfUtils.getXImageDataForPage(int page);
     *     }
     *     pdfUtils.closePDFfile();
     </code></pre>
     *
     @param page logical page in pdf file
     *
     @return The image data on the page
     */
    public String getXImageDataForPage(final int page) {

        ImageCommands.trackImages = true;

        checkFileOpened();  // creates instance of decode_pdf

        decode_pdf.decodePage(page);

        ImageCommands.trackImages = false;

        return decode_pdf.getInfo(PdfDictionary.Image);
    }

    /**
     * get XObject data on pages in PDF file (starting at 1)
     *
     <pre><code>
     *     PdfUtilities pdfUtils = new PdfUtilities("C:/pdfs/mypdf.pdf");
     *     // pdfUtils.setPassword("password");
     *     if (pdfUtils.openPDFFile()) {
     *         String pageXObjects = pdfUtils.getXObjectsForPage(int page);
     *     }
     *
     *     pdfUtils.closePDFfile();
     </code></pre>
     *
     @param page logical page in pdf file
     *
     @return The XObject data on the page
     */
    public String getXObjectsForPage(final int page) {

        checkFileOpened();  // creates instance of decode_pdf

        return decode_pdf.getIO().getXObjectsForPage(page);
    }

    /**
     * return a String detailing the fonts from page
     *
     <pre><code>
     *     PdfUtilities pdfUtils=new PdfUtilities("C:/pdfs/mypdf.pdf");
     *     // pdfUtils.setPassword("password");
     *     if (pdfUtils.openPDFFile()) {
     *         String fontDetailsOnPage = pdfUtils.getFontDataForPage(int oage);
     *     }
     *
     *     pdfUtils.closePDFfile();
     </code></pre>
     *
     @param page pdf logical page
     *
     @return the fonts on the page
     */
    public String getFontDataForPage(final int page) {

        checkFileOpened();  // creates instance of decode_pdf

        decode_pdf.decodePage(page);

        return decode_pdf.getInfo(PdfDictionary.Font);
    }

    public Map<Integer, String> getAllFontDataForDocument() {

        ImageCommands.trackImages = true;

        final Map<Integer, String> font_data = new HashMap<>();
        checkFileOpened();  // creates instance of decode_pdf

        for (int page = 1; page <= getPageCount(); page++) {
            decode_pdf.decodePage(page);

            font_data.put(page, decode_pdf.getInfo(PdfDictionary.Font));

        }

        return font_data;
    }

    /**
     * ensure PDF file is closed once no longer needed and all resources released
     */
    @Override
    public void closePDFfile() {

        checkFileOpened();

        super.closePDFfile();

        //reset in case instance reused
        hasEmbeddedFonts = false;
        testIfFontsEmbedded = false;

    }

    /**
     * return boolean to show if Pdf file contains embedded fonts
     *
     <pre><code>
     *     PdfUtilities pdfUtils=new PdfUtilities("C:/pdfs/mypdf.pdf");
     *     // pdfUtils.setPassword("password");
     *     if (pdfUtils.openPDFFile()) {
     *         boolean usesEmbeddedFonts=pdfUtils.hasEmbeddedFonts();
     *     }
     *     pdfUtils.closePDFfile();
     </code></pre>
     *
     @return {@code true} if present in pdf file
     */
    public boolean hasEmbeddedFonts() {

        checkFileOpened();

        if (!testIfFontsEmbedded) {

            testIfFontsEmbedded = true;

            final int pageCount = getPageCount();

            for (int page = 1; page < pageCount + 1; page++) {
                decode_pdf.decodePage(page);

                hasEmbeddedFonts = decode_pdf.hasEmbeddedFonts();

                // exit on first true
                if (hasEmbeddedFonts) {
                    page = pageCount;
                }
            }
        }

        return hasEmbeddedFonts;
    }

    /**
     * return a Map containing the pair value properties (if present) from the open PDF file -
     *
     <pre><code>
     *     PdfUtilities pdfUtils = new PdfUtilities("C:/pdfs/mypdf.pdf");
     *     // pdfUtils.setPassword("password");
     *     if (pdfUtils.openPDFFile()) {
     *         Map mapOfValuePairs = pdfUtils.getDocumentPropertyStringValuesAsMap();
     *     }
     *
     *     pdfUtils.closePDFfile();
     </code></pre>
     *
     @return A map of document properties from the PDF file
     */
    public Map<String, String> getDocumentPropertyStringValuesAsMap() {

        checkFileOpened();

        final String[] names = PdfFileInformation.getFieldNames();
        final int count = names.length;

        final String[] keys = currentFileInformation.getFieldValues();

        final Map<String, String> map = new HashMap<>();
        for (int i = 0; i < count; i++) {
            map.put(names[i], keys[i]);
        }

        return map;
    }

    /**
     * access the XML Document properties String (if present) from the open PDF file
     *
     <pre><code>
     *     PdfUtilities pdfUtils = new PdfUtilities("C:/pdfs/mypdf.pdf");
     *     // pdfUtils.setPassword("password");
     *     if (pdfUtils.openPDFFile()) {
     *          String XMLStringData = pdfUtils.getDocumentPropertyFieldsInXML();
     *     }
     *
     *     pdfUtils.closePDFfile();
     </code></pre>
     *
     @return String containing the raw text data
     */
    public String getDocumentPropertyFieldsInXML() {

        checkFileOpened();

        return currentFileInformation.getFileXMLMetaData();

    }

    /**
     <pre><code>
     *     PdfUtilities pdfUtils = new PdfUtilities("C:/pdfs/mypdf.pdf");
     *     // pdfUtils.setPassword("password");
     *     if (pdfUtils.openPDFFile()) {
     *         float[] pageDimensions = pdfUtils.getPageDimensions(pageNum, PageUnits.Inches, PageSizeType.CropBox);
     *     }
     *
     *     pdfUtils.closePDFfile();
     </code></pre>
     *
     @param page  is pageNumber
     @param units units to use for pageSize dimensions (pixels, Inches, Centimetres) except rotation which is always
     *              in degrees
     @param type  which values to select (MediaBox, CropBox)
     *
     @return a float[] with 5 values:- x,y,w,h, pageRotation
     */
    public float[] getPageDimensions(final int page, final PageUnits units, final PageSizeType type) {

        checkFileOpened();

        final float[] pageSize = new float[5];

        final float factor = switch (units) {
            case Pixels -> 1f;
            case Inches -> 72f;
            case Centimetres -> 72f 2.54f;
        };

        switch (type) {
            case MediaBox:
                pageSize[0= currentPageData.getMediaBoxX(page/ factor;
                pageSize[1= currentPageData.getMediaBoxY(page/ factor;
                pageSize[2= currentPageData.getMediaBoxWidth(page/ factor;
                pageSize[3= currentPageData.getMediaBoxHeight(page/ factor;
                break;

            case CropBox:
                pageSize[0= currentPageData.getCropBoxX(page/ factor;
                pageSize[1= currentPageData.getCropBoxY(page/ factor;
                pageSize[2= currentPageData.getCropBoxWidth(page/ factor;
                pageSize[3= currentPageData.getCropBoxHeight(page/ factor;

                break;
        }

        pageSize[4= currentPageData.getRotation(page);

        return pageSize;
    }

    private void checkFileOpened() {
        if (!isOpen) {
            throw new RuntimeException("PDF file needs to be opened with openPDFFile() method first");
        }
    }

    /**
     <pre><code>
     *     PdfUtilities pdfUtils = new PdfUtilities("C:/pdfs/mypdf.pdf");
     *     // pdfUtils.setPassword("password");
     *     if (pdfUtils.isPDFLinearized()) {
     *         // file is Linearized and has FastWeb view
     *     }
     *
     *     pdfUtils.closePDFfile();
     </code></pre>
     *
     @return a boolean to show if file is Linearized
     */
    public boolean isPDFLinearized() {
        return (decode_pdf != null && decode_pdf.getJPedalObject(PdfDictionary.Linearized!= null);
    }

    /**
     <pre><code>
     *     PdfUtilities pdfUtils = new PdfUtilities("C:/pdfs/mypdf.pdf");
     *     // pdfUtils.setPassword("password");
     *     if (pdfUtils.openPDFFile()) {
     *         int P = pdfUtils.getPdfFilePermissions();
     *     }
     *
     *     pdfUtils.closePDFfile();
     </code></pre>
     *
     @return an unsigned 32-bit integer containing a set of permission flags
     */
    public int getPdfFilePermissions() {
        int P = -1;
        if (decode_pdf != null) {
            final PdfObject encryptObj = decode_pdf.getIO().getPDFObject(PdfDictionary.Encrypt);
            if (encryptObj != null) {
                P = encryptObj.getInt(PdfDictionary.P);
            }
        }
        return P;
    }

    /**
     <pre><code>
     *     PdfUtilities pdfUtils = new PdfUtilities("C:/pdfs/mypdf.pdf");
     *     // pdfUtils.setPassword("password");
     *     if (pdfUtils.openPDFFile()) {
     *         boolean isMarked = pdfUtils.isMarkedContent();
     *     }
     *
     *     pdfUtils.closePDFfile();
     </code></pre>
     *
     @return {@code true} if fully meets MarkedContent specification - may still be able to extract something if
     *         false
     */
    public boolean isMarkedContent() {
        boolean isMarked = false;
        if (decode_pdf != null) {
            isMarked = new MarkedContentGenerator((PdfResourcesdecode_pdf.getJPedalObject(PdfDictionary.Resources)new ExternalHandlers(), decode_pdf.getDecoderOptions()).isMarkedContent();
        }
        return isMarked;
    }

    /**
     <pre><code>
     *     PdfUtilities pdfUtils = new PdfUtilities("C:/pdfs/mypdf.pdf");
     *     // pdfUtils.setPassword("password");
     *     if (pdfUtils.openPDFFile()) {
     *         int P = pdfUtils.getPdfFilePermissions();
     *         PdfUtilities.showPermissionsAsString(P);
     *     }
     *
     *     pdfUtils.closePDFfile();
     </code></pre>
     *
     @param P a 32-bit integer containing permission flags extracted from PDF
     */
    public static void showPermissionsAsString(final int P) {

        //power of 2 lookup table
        final int[] lookup = new int[32];
        int power = 1;
        for (int ptr = 0; ptr < 32; ptr++) {
            lookup[ptr= power;
            power *= 2;
        }

        System.out.println("PDF Security settings in Encryption Object");
        System.out.println("------------------------------------------");
        System.out.println("raw P value = " + Integer.toBinaryString(P));

        final Object[] flags = {3"Printing"4"Modify"5"Copy/extract"6"Add/modify Annotation",
                9"Fill in existing forms"10"Extraction for accessibility"11"Document Assembly"12"Printing High-quality"};

        final int count = flags.length;
        for (int ptr = 0; ptr < count; ptr += 2) {

            final int bitToTest = (Integerflags[ptr];
            final String value = (Stringflags[ptr + 1];
            if ((P & lookup[bitToTest]) == lookup[bitToTest]) {
                System.out.println(value + " (bit " + bitToTest + ") is allowed");
            else {
                System.out.println(value + " (bit " + bitToTest + ") is NOT allowed");
            }
        }
    }

    @Override
    protected void decodeFile(final String rootDirthrows PdfException {
        //Thie example does not require the file to be decoded
    }

}