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

import org.jpedal.PdfDecoderInt;
import org.jpedal.PdfDecoderServer;
import org.jpedal.exception.PdfException;
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 {

    /**
     * return a count of Postscript commands from page -
     *
     @param page pdf logical page
     @return
     <pre>PdfUtilities pdfUtils=new PdfUtilities("C:/pdfs/mypdf.pdf");
     * //pdfUtils.setPassword("password");
     * if (pdfUtils.openPDFFile()) {
     *  int commandsOnPage=pdfUtils.getCommandCountForPageStream();
     * }
     *
     * pdfUtils.closePDFfile();</pre>
     */
    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 byte[] byteArray;

    private boolean isOpen;

    private boolean hasEmbeddedFonts;
    private boolean testIfFontsEmbedded;

    private PdfPageData currentPageData;

    /**
     * the decoder object which decodes the pdf and returns a data object
     */
    private PdfDecoderInt decode_pdf;

    private String password;

    private String fileName;


    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
    }

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

        checkFileOpened();

        return decode_pdf.getPageCount();
    }

    /**
     * get image data on pages in PDF file (starting at 1)
     * (Method IS NOT THREADSAFE)
     *
     @param page logical page in pdf file
     @return page count
     <pre>PdfUtilities pdfUtils=new PdfUtilities("C:/pdfs/mypdf.pdf");
     * //pdfUtils.setPassword("password");
     * if (pdfUtils.openPDFFile()) {
     * String pageImages=pdfUtils.getXImageDataForPage(int page);
     * }
     *
     * pdfUtils.closePDFfile();</pre>
     */
    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)
     *
     @param page logical page in pdf file
     @return page count
     <pre>PdfUtilities pdfUtils=new PdfUtilities("C:/pdfs/mypdf.pdf");
     * //pdfUtils.setPassword("password");
     * if (pdfUtils.openPDFFile()) {
     * String pageXObjects = pdfUtils.getXObjectsForPage(int page);
     * }
     *
     * pdfUtils.closePDFfile();</pre>
     */
    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 -
     *
     @param page pdf logical page
     @return
     <pre>PdfUtilities pdfUtils=new PdfUtilities("C:/pdfs/mypdf.pdf");
     * //pdfUtils.setPassword("password");
     * if (pdfUtils.openPDFFile()) {
     *  String fontDetailsOnPage = pdfUtils.getFontDataForPage(int oage);
     * }
     *
     * pdfUtils.closePDFfile();</pre>
     */
    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;
    }

    /**
     * routine to open the PDF File so we can access
     *
     @return true if successful
     @throws PdfException if problem with opening a PDF file
     */
    public boolean openPDFFile() throws PdfException {

        isOpen = false;

        try {
            if (fileName != null) {
                if (password == null) {
                    decode_pdf.openPdfFile(fileName);
                else {
                    decode_pdf.openPdfFile(fileName, password);
                }
            else if (byteArray != null) {
                if (password == null) {
                    decode_pdf.openPdfArray(byteArray);
                else {
                    decode_pdf.openPdfArray(byteArray, password);
                }
            }

            /*
             * check security settings
             */
            if (decode_pdf.isEncrypted() && !decode_pdf.isPasswordSupplied()) {

                if (password == null) {
                    throw new PdfException("Unable to open encrypted PDF file - call setPassword(passsword) ");
                else {
                    throw new PdfException("Unable to open encrypted PDF file with password " + password);
                }
            else {

                isOpen = true;

                /*get the Pdf file information object to extract info from*/
                currentFileInformation = decode_pdf.getFileInformationData();
                currentPageData = decode_pdf.getPdfPageData();
            }
        catch (final PdfException e) {
            throw new PdfException(e.getMessage(), e);
        }

        return isOpen;
    }

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

        checkFileOpened();

        if (decode_pdf != null && decode_pdf.isOpen()) {

            decode_pdf.flushObjectValues(true);

            decode_pdf.closePdfFile();
        }

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

    }

    /**
     * return boolean to show if Pdf file contains embedded fonts
     *
     <pre>PdfUtilities pdfUtils=new PdfUtilities("C:/pdfs/mypdf.pdf");
     * //pdfUtils.setPassword("password");
     * if (pdfUtils.openPDFFile()) {
     *  boolean usesEmbeddedFonts=pdfUtils.hasEmbeddedFonts();
     * }
     *
     * pdfUtils.closePDFfile();</pre>
     @return flag to show 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 -
     *
     @return
     <pre>PdfUtilities pdfUtils=new PdfUtilities("C:/pdfs/mypdf.pdf");
     * //pdfUtils.setPassword("password");
     * if (pdfUtils.openPDFFile()) {
     *  Map mapOfValuePairs=pdfUtils.getDocumentPropertyStringValuesAsMap();
     * }
     *
     * pdfUtils.closePDFfile();</pre>
     */
    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
     *
     @return String containing the raw text data
     *
     <pre>PdfUtilities pdfUtils=new PdfUtilities("C:/pdfs/mypdf.pdf");
     * //pdfUtils.setPassword("password");
     * if (pdfUtils.openPDFFile()) {
     *  String XMLStringData=pdfUtils.getDocumentPropertyFieldsInXML();
     * }
     *
     * pdfUtils.closePDFfile();</pre>
     */
    public String getDocumentPropertyFieldsInXML() {

        checkFileOpened();

        return currentFileInformation.getFileXMLMetaData();

    }

    /**
     @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
     *
     <pre>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();</pre>
     */
    public float[] getPageDimensions(final int page, final PageUnits units, final PageSizeType type) {

        checkFileOpened();

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

        float factor = 0f;

        switch (units) {
            case Pixels:
                factor = 1f;
                break;
            case Inches:
                factor = 72f;
                break;
            case Centimetres:
                factor = 72f 2.54f;
                break;
        }

        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");
        }
    }

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

    /**
     @return an unsigned 32-bit integer containing a set of permission flags
     *
     <pre>PdfUtilities pdfUtils=new PdfUtilities("C:/pdfs/mypdf.pdf");
     * //pdfUtils.setPassword("password");
     * if (pdfUtils.openPDFFile()) {
     *  int P = pdfUtils.getPdfFilePermissions();
     * }
     *
     * pdfUtils.closePDFfile();</pre>
     */
    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;
    }

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

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