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

import org.jpedal.PdfDecoder;
import org.jpedal.PdfDecoderInt;
import org.jpedal.color.PdfPaint;
import org.jpedal.examples.viewer.gui.SwingGUI;
import org.jpedal.examples.viewer.gui.popups.PrintPanel;
import org.jpedal.exception.PdfException;
import org.jpedal.external.ColorHandler;
import org.jpedal.external.Options;
import org.jpedal.objects.PrinterOptions;
import org.jpedal.utils.LogWriter;
import org.jpedal.utils.Messages;

import javax.print.Doc;
import javax.print.DocFlavor;
import javax.print.DocPrintJob;
import javax.print.PrintException;
import javax.print.PrintService;
import javax.print.SimpleDoc;
import javax.print.attribute.HashPrintRequestAttributeSet;
import javax.print.attribute.PrintRequestAttributeSet;
import javax.print.attribute.SetOfIntegerSyntax;
import javax.print.attribute.standard.Chromaticity;
import javax.print.attribute.standard.Copies;
import javax.print.attribute.standard.JobName;
import javax.print.attribute.standard.PageRanges;
import javax.print.attribute.standard.PrinterResolution;
import javax.print.event.PrintJobEvent;
import javax.print.event.PrintJobListener;
import javax.swing.JComponent;
import javax.swing.JOptionPane;
import javax.swing.ProgressMonitor;
import javax.swing.Timer;
import java.awt.Color;
import java.awt.Component;
import java.awt.Graphics2D;
import java.awt.color.ColorSpace;
import java.awt.image.BufferedImage;
import java.awt.print.PageFormat;
import java.awt.print.Paper;
import java.awt.print.PrinterJob;

public class Printer {

    //page range to print
    private int rangeStart = 1;
    private int rangeEnd = 1;

    //type of printing - all, odd, even
    private int subset = PrinterOptions.ALL_PAGES;

    //Check to see if Printing cancelled
    private boolean wasCancelled;

    //Allow Printing Cancelled to appear once
    private boolean messageShown;

    private boolean pagesReversed;

    //provide user with visual clue to print progress
    private Timer updatePrinterProgress;

    private ProgressMonitor status;

    // needs to be global for the printer selection to work 
    private DocPrintJob printJob;

    // for use with fest testing
    private static final boolean showOptionPane = true;

    public void printPDF(final PdfDecoderInt decodePdf, final SwingGUI currentGUI, final String blacklist, final String defaultPrinter) {

        //provides atomic flag on printing so we don't exit until all done
        PrintStatus.printingThreads++;

        //printing in thread to improve background printing
        final Thread worker = new Thread(() -> {

            final boolean printFile = printing(decodePdf, currentGUI, defaultPrinter, blacklist);

            //visual print update progress box
            if (updatePrinterProgress != null) {
                updatePrinterProgress.stop();
                status.close();
            }

            //report any errors (done it this way as MAC OS X has issue with PrinterException)
            if (!printFile && !((PdfDecoderdecodePdf).isPageSuccessful()) {
                showPrintErrorMessage(decodePdf, currentGUI);
            }

            PrintStatus.printingThreads--;

            //redraw to clean up
            ((PdfDecoderdecodePdf).resetCurrentPrintPage();
            ((ComponentdecodePdf).invalidate();
            ((JComponentdecodePdf).updateUI();
            decodePdf.repaint();


            if (printFile && !wasCancelled && showOptionPane) {
                currentGUI.showMessageDialog(Messages.getMessage("PdfViewerPrintingFinished"));
            }
        });
        worker.setDaemon(true);
        //start printing in background (comment out if not required)
        worker.start();

    }

    private boolean printing(final PdfDecoderInt decodePdf, final SwingGUI currentGUI, final String defaultPrinter, final String blacklist) {

        boolean printFile = false;

        try {

            final PageFormat pf = createDefaultPageFormat();

            /*
             * workaround to improve performance on PCL printing
             * by printing using drawString or Java's glyph if font
             * available in Java
             */
            //decode_pdf.setTextPrint(PdfDecoder.NOTEXTPRINT); //normal mode - only needed to reset
            //decode_pdf.setTextPrint(PdfDecoder.TEXTGLYPHPRINT); //intermediate mode - let Java create Glyphs if font matches
            //decode_pdf.setTextPrint(PdfDecoder.TEXTSTRINGPRINT); //try and get Java to do all the work

            //wrap in Doc as we can then add a listeners
            final Doc doc = new SimpleDoc(decodePdf, DocFlavor.SERVICE_FORMATTED.PAGEABLE, null);

            //setup default values to padd into JPS
            final PrintRequestAttributeSet aset = new HashPrintRequestAttributeSet();
            aset.add(new PageRanges(1, decodePdf.getPageCount()));

            // useful debugging code to output actual data sent to the printer spool
            //aset.add(new javax.print.attribute.standard.Destination(new java.net.URI("file:///C:/testfiles/printSpool.txt")));

            // useful debugging code to show supported values and values returned by printer
            //Attribute[] settings = aset.toArray();
            //Class[] attribs=printJob.getPrintService().getSupportedAttributeCategories();
            //for(int i=0;i<attribs.length;i++)
            //System.out.println(i+" "+attribs[i]);
            //for(int i=0;i<settings.length;i++) //show values set by printer
            //    System.out.println(i+" "+settings[i].toString()+" "+settings[i].getName());

            //custom dialog so we can copy Acrobat PDF settings
            final PrintPanel printPanel = currentGUI.printDialog(PrintStatus.getAvailablePrinters(blacklist), defaultPrinter);

            printFile = printPanel.okClicked();

            //ensure PDF display reappears
            decodePdf.repaint();

            // choose the printer, testing if printer in list
            setPrinter(printPanel.getPrinter());

                         setPrintModeFromPrintPanel(printPanel, decodePdf);

            //can also take values such as  new PageRanges("3,5,7-9,15");
            final SetOfIntegerSyntax range = printPanel.getPrintRange();

            //store color handler in case it needs to be replaced for grayscale printing
            final Object storedColorHandler = decodePdf.getExternalHandler(Options.ColorHandler);

            if (range == null) {
                currentGUI.showMessageDialog("No pages to print");
            else {
                setPrintRange(decodePdf, range);

                setPrintRenderSettings(aset, printPanel, decodePdf, pf);
            }

            //popup to show user progress
            if (showOptionPane) {
                displayProgressPane(decodePdf, currentGUI);
            }

            //Name the print job the same as the Pdf file.
            if (decodePdf.getFileName() != null) { //can be null if we pass in PDF as byte[] array
                final String[] jobString = decodePdf.getFileName().split("/");
                final JobName jobName = new JobName(jobString[jobString.length - 1]null);
                if (printJob.getPrintService().isAttributeValueSupported(jobName, DocFlavor.SERVICE_FORMATTED.PAGEABLE, aset)) {
                    aset.add(jobName);
                }
            }

            //actual print call
            if (printFile) {
                //used to track print activity
                printJob.addPrintJobListener(new PDFPrintJobListener());

                //Print PDF document
                printJob.print(doc, aset);
            }

            //Restore color handler in case grayscale printing was used
            decodePdf.addExternalHandler(storedColorHandler, Options.ColorHandler);

        catch (final IllegalArgumentException | PrintException | PdfException e) {
            LogWriter.writeLog("Exception " + e + " printing");
            currentGUI.showMessageDialog("Exception " + e);
        }

        return printFile;
    }

    private void displayProgressPane(final PdfDecoderInt decodePdf, final SwingGUI currentGUI) {
        status = new ProgressMonitor(currentGUI.getFrame()""""1100);

        // used to track user stopping movement and call refresh every 2 seconds
        updatePrinterProgress = new Timer(1_000, event -> {

            final int currentPage = ((PdfDecoderdecodePdf).getCurrentPrintPage();

            if (currentPage > 0) {
                updatePrinterProgess(decodePdf, currentPage);
            }

            //make sure turned off
            if (currentPage == -1) {
                updatePrinterProgress.stop();
                status.close();
            }
        });
        updatePrinterProgress.setRepeats(true);
        updatePrinterProgress.start();
    }

    private static void showPrintErrorMessage(final PdfDecoderInt decodePdf, final SwingGUI currentGUI) {
        final StringBuilder builder = new StringBuilder(Messages.getMessage("PdfViewerError.ProblemsEncountered"));
        builder.append(((PdfDecoderdecodePdf).getPageFailureMessage());
        builder.append('\n');

        if (((PdfDecoderdecodePdf).getPageFailureMessage().toLowerCase().contains("memory")) {
            builder.append(Messages.getMessage("PdfViewerError.RerunJava"));
            builder.append(Messages.getMessage("PdfViewerError.RerunJava1"));
            builder.append(Messages.getMessage("PdfViewerError.RerunJava2"));
        }

        currentGUI.showMessageDialog(builder.toString());
    }

    private static void setPrintRenderSettings(final PrintRequestAttributeSet aset, final PrintPanel printPanel, final PdfDecoderInt decodePdf, final PageFormat pf) {

        //pass through number of copies
        aset.add(new Copies(printPanel.getCopies()));

        
        //Auto-rotate and scale flag
        ((PdfDecoderdecodePdf).setPrintAutoRotateAndCenter(printPanel.isAutoRotateAndCenter());

        
        // Are we printing the current area only
        ((PdfDecoderdecodePdf).setPrintCurrentView(printPanel.isPrintingCurrentView());

        
        //set mode - see org.jpedal.objects.contstants.PrinterOptions for all values
        ((PdfDecoderdecodePdf).setPrintPageScalingMode(printPanel.getPageScaling());

        
        //Set whether to print in monochrome or full color
        if (printPanel.isMonochrome()) {
            aset.remove(Chromaticity.COLOR);
            aset.add(Chromaticity.MONOCHROME);
            decodePdf.addExternalHandler(new MonochromeColorHandler(), Options.ColorHandler);
        else {
            aset.remove(Chromaticity.MONOCHROME);
            aset.add(Chromaticity.COLOR);
        }

        //set paper size
        if (printPanel.getSelectedPaper() != null) {
            pf.setPaper(printPanel.getSelectedPaper());
        }
        //Set paper orientation
        pf.setOrientation(printPanel.getSelectedPrinterOrientation());

        ((PdfDecoderdecodePdf).setPageFormat(pf);

        
        // flag if we use paper size or PDF size
        ((PdfDecoderdecodePdf).setUsePDFPaperSize(printPanel.isPaperSourceByPDFSize());

        //Set print resolution
        final PrinterResolution res = printPanel.getResolution();
        if (res != null) {
            aset.add(res);
        }

    }

    private void setPrintRange(final PdfDecoderInt decodePdf, final SetOfIntegerSyntax rangethrows PdfException {
        ((PdfDecoderdecodePdf).setPagePrintRange(range);

        // workout values for progress monitor
        rangeStart = range.next(0)// find first

        // find last
        int i = rangeStart;
        rangeEnd = rangeStart;
        if (range.contains(2_147_483_647)) { //allow for all returning largest int
            rangeEnd = decodePdf.getPageCount();
        else {
            while (range.next(i!= -1) {
                i++;
            }
            rangeEnd = i;
        }
    }

    private void setPrintModeFromPrintPanel(final PrintPanel printPanel, final PdfDecoderInt decodePdf) {
        //range of pages
        int printMode = 0;
        subset = PrinterOptions.ALL_PAGES;
        if (printPanel.isOddPagesOnly()) {
            printMode = PrinterOptions.ODD_PAGES_ONLY;
            subset = PrinterOptions.ODD_PAGES_ONLY;
        else if (printPanel.isEvenPagesOnly()) {
            printMode = PrinterOptions.EVEN_PAGES_ONLY;
            subset = PrinterOptions.EVEN_PAGES_ONLY;
        }

        //flag to show reversed
        pagesReversed = printPanel.isPagesReversed();
        if (pagesReversed) {
            printMode += PrinterOptions.PRINT_PAGES_REVERSED;
        }

        ((PdfDecoderdecodePdf).setPrintPageMode(printMode);
    }

    private static PageFormat createDefaultPageFormat() {
        final PageFormat pf = PrinterJob.getPrinterJob().defaultPage();

        
        //default page size
        final Paper paper = new Paper();
        paper.setSize(595842);
        paper.setImageableArea(4343509756);

        pf.setPaper(paper);

        return pf;
    }

    //visual print indicator
    private String dots = ".";

    private void updatePrinterProgess(final PdfDecoderInt decode_pdf, final int currentPage) {


        //Calculate no of pages printing
        int noOfPagesPrinting = (rangeEnd - rangeStart + 1);

        //Calculate which page we are currently printing
        final int currentPrintingPage = (currentPage - rangeStart);

        int actualCount = noOfPagesPrinting;
        int actualPage = currentPrintingPage;
        int actualPercentage = (int) ((actualPage / (floatactualCount100);


        if (status.isCanceled()) {
            ((PdfDecoderdecode_pdf).stopPrinting();
            updatePrinterProgress.stop();
            status.close();
            wasCancelled = true;
            PrintStatus.printingThreads--;

            if (!messageShown) {
                JOptionPane.showMessageDialog(null, Messages.getMessage("PdfViewerPrint.PrintingCanceled"));
                messageShown = true;
            }
            return;

        }


        //update visual clue
        dots += '.';
        if (dots.length() 8) {
            dots = ".";
        }

        //allow for backwards
        boolean isBackwards = ((currentPrintingPage <= 0));

        if (rangeStart == rangeEnd) {
            isBackwards = false;
        }

        if ((isBackwards)) {
            noOfPagesPrinting = (rangeStart - rangeEnd + 1);
        }

        int percentage = (int) ((currentPrintingPage / (floatnoOfPagesPrinting100);

        if ((!isBackwards&& (percentage < 1)) {
            percentage = 1;
        }


        //invert percentage so percentage works correctly
        if (isBackwards) {
            percentage = -percentage;
        }

        if (pagesReversed) {
            percentage = 100 - percentage;
        }

        status.setProgress(percentage);
        String message;

        if (subset == PrinterOptions.ODD_PAGES_ONLY) {
            actualCount = ((actualCount / 21);
            actualPage /= 2;

        else if (subset == PrinterOptions.EVEN_PAGES_ONLY) {
            actualCount = ((actualCount / 21);
            actualPage /= 2;

        }
        
        /*
         * allow for printing 1 page
         * Set to page 1 of 1 like Adobe
         */
        if (actualCount == 1) {
            actualPercentage = 50;
            actualPage = 1;
            status.setProgress(actualPercentage);
        }

        message = actualPage + " " + Messages.getMessage("PdfViewerPrint.Of"' ' +
                actualCount + ": " + actualPercentage + '%' ' ' + dots;

        if (pagesReversed) {
            message = (actualCount - actualPage" " + Messages.getMessage("PdfViewerPrint.Of"' ' +
                    actualCount + ": " + percentage + '%' ' ' + dots;

            status.setNote(Messages.getMessage("PdfViewerPrint.ReversedPrinting"' ' + message);

        else if (isBackwards) {
            status.setNote(Messages.getMessage("PdfViewerPrint.ReversedPrinting"' ' + message);
        else {
            status.setNote(Messages.getMessage("PdfViewerPrint.Printing"' ' + message);
        }

    }

    

    
    private void setPrinter(final String chosenPrinterthrows PdfException {

        final PrintService[] service = PrinterJob.lookupPrintServices()//list of printers

        final int count = service.length;
        boolean matchFound = false;

        for (int i = 0; i < count; i++) {
            if (service[i].getName().contains(chosenPrinter)) {
                printJob = service[i].createPrintJob();
                i = count;
                matchFound = true;
            }
        }
        if (!matchFound) {
            throw new PdfException("Unknown printer " + chosenPrinter);
        }
    }

    /**
     * listener code - just an example
     */
    private static class PDFPrintJobListener implements PrintJobListener {

        private static final boolean showMessages = false;

        @Override
        public void printDataTransferCompleted(final PrintJobEvent printJobEvent) {
            if (showMessages) {
                System.out.println("printDataTransferCompleted=" + printJobEvent);
            }
        }

        @Override
        public void printJobCompleted(final PrintJobEvent printJobEvent) {
            if (showMessages) {
                System.out.println("printJobCompleted=" + printJobEvent);
            }
        }

        @Override
        public void printJobFailed(final PrintJobEvent printJobEvent) {
            if (showMessages) {
                System.out.println("printJobEvent=" + printJobEvent);
            }
        }

        @Override
        public void printJobCanceled(final PrintJobEvent printJobEvent) {
            if (showMessages) {
                System.out.println("printJobFailed=" + printJobEvent);
            }
        }

        @Override
        public void printJobNoMoreEvents(final PrintJobEvent printJobEvent) {
            if (showMessages) {
                System.out.println("printJobNoMoreEvents=" + printJobEvent);
            }
        }

        @Override
        public void printJobRequiresAttention(final PrintJobEvent printJobEvent) {
            if (showMessages) {
                System.out.println("printJobRequiresAttention=" + printJobEvent);
            }
        }
    }

    static class MonochromeColorHandler implements ColorHandler {

        @Override
        public void setPaint(final Graphics2D g2, final PdfPaint textFillCol, final int pageNumber, final boolean isPrinting) {
            //converts to grayscale for printing
            if (isPrinting && textFillCol != null) { //only on printout

                final int rgb = textFillCol.getRGB();

                //get the value
                final float[] val = new float[3];
                val[0((rgb >> 16255255f;
                val[1((rgb >> 8255255f;
                val[2(rgb & 255255f;

                //to gray
                final ColorSpace cs = ColorSpace.getInstance(ColorSpace.CS_GRAY);
                final float[] grayVal = cs.fromRGB(val);

                final Color colGray = new Color(cs, grayVal, 1f);
                g2.setPaint(colGray);

            else {
                g2.setPaint(textFillCol);
            }
        }

        @Override
        public BufferedImage processImage(final BufferedImage image, final int pageNumber, final boolean isPrinting) {

            if (isPrinting && image != null) { //only on printout

                //grayscale conversion
                final BufferedImage newImage = new BufferedImage(image.getWidth(), image.getHeight(), BufferedImage.TYPE_BYTE_GRAY);
                final Graphics2D newG2 = newImage.createGraphics();
                newG2.setPaint(Color.WHITE);
                newG2.fillRect(00, image.getWidth(), image.getHeight());
                newG2.drawImage(image, 00null);

                return newImage;
            }
            return image;
        }
    }
}