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

import org.jpedal.PdfDecoder;
import org.jpedal.PdfDecoderInt;
import org.jpedal.examples.viewer.commands.OpenFile;
import org.jpedal.examples.viewer.gui.SwingGUI;
import org.jpedal.examples.viewer.gui.swing.SearchList;
import org.jpedal.examples.viewer.gui.swing.SwingSearchWindow;
import org.jpedal.examples.viewer.gui.swing.SwingThumbnailPanel;
import org.jpedal.examples.viewer.objects.SwingClientExternalHandler;
import org.jpedal.examples.viewer.utils.PropertiesFile;
import org.jpedal.exception.PdfException;
import org.jpedal.external.Options;
import org.jpedal.fonts.FontMappings;
import org.jpedal.objects.acroforms.actions.ActionHandler;
import org.jpedal.objects.acroforms.actions.DefaultActionHandler;
import org.jpedal.objects.raw.OutlineObject;
import org.jpedal.objects.raw.PdfDictionary;
import org.jpedal.objects.raw.PdfObject;
import org.jpedal.parser.DecoderOptions;
import org.jpedal.utils.LogWriter;
import org.jpedal.utils.Messages;
import org.w3c.dom.Node;

import javax.swing.SwingUtilities;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.util.ResourceBundle;


/**
 <h2><b>PDF Viewer</b></h2>
 <p>
 <p>If you are compiling, you will need to download all the examples source files from :
 * <a href="http://www.idrsolutions.com/how-to-view-pdf-files-in-java/">How to View PDF File in Java.</a></p>
 <p>
 <p><b>Run directly from jar with java -cp jpedal.jar org/jpedal/examples/viewer/Viewer
 * or java -jar jpedal.jar</b></p>
 <p>
 <p>There are plenty of tutorials on how to configure the Viewer on our website <a href="http://www.idrsolutions.com/java-pdf-library-support/">Support Section.</a></p>
 <p>
 <p><a href="http://javadoc.idrsolutions.com/org/jpedal/constants/JPedalSettings.html">See Here for Settings to Customise the PDF Viewer</a></p>
 <p>
 <p>If you want to implement your own there is
 * a very simple example at :
 * <a href="http://files.idrsolutions.com/samplecode/org/jpedal/examples/jpaneldemo/JPanelDemo.java.html">JPanel Demo.</a></p>
 <p>We recommend you look at the full viewer as it is totally configurable and does everything for you.</p>
 <p>
 <p>
 <p>Fully featured GUI viewer and demonstration of JPedal's capabilities.</p>
 <p>
 <p>This class provides the framework for the Viewer and calls other classes which provide the following
 * functions:-</p>
 <ul>
 <li>Values commonValues - repository for general settings.</li>
 <li>PdfDecoder decode_pdf - PDF library and panel.</li>
 <li>SwingThumbnailPanel thumbnails - provides a thumbnail pane down the left side of page - thumbnails can be clicked on to goto page.</li>
 <li>PropertiesFile properties - saved values stored between sessions.</li>
 <li>SwingGUI currentGUI - all Swing GUI functions.</li>
 <li>SwingSearchWindow searchFrame (not GPL) - search Window to search pages and goto references on any page.</li>
 <li>SwingCommands currentCommands - parses and executes all options.</li>
 </ul>
 */
public class Viewer {

    //Location of Preferences Files
    public static final String PREFERENCES_DEFAULT = "jar:/org/jpedal/examples/viewer/res/preferences/Default.xml";
    public static final String PREFERENCES_NO_GUI = "jar:/org/jpedal/examples/viewer/res/preferences/NoGUI.xml";
    public static final String PREFERENCES_NO_SIDE_BAR = "jar:/org/jpedal/examples/viewer/res/preferences/NoSideTabOrTopButtons.xml";
    public static final String PREFERENCES_OPEN_AND_NAV_ONLY = "jar:/org/jpedal/examples/viewer/res/preferences/OpenAndNavOnly.xml";
    //public static final String PREFERENCES_BEAN = "jar:/org/jpedal/examples/viewer/res/preferences/Bean.xml";

    //repository for general settings
    private Values commonValues = new Values();

    //PDF library and panel
    PdfDecoderInt decode_pdf;

    //encapsulates all thumbnail functionality - just ignore if not required
    private SwingThumbnailPanel thumbnails;

    //values saved on file between sessions
    private PropertiesFile properties = new PropertiesFile();

    //general GUI functions
    protected SwingGUI currentGUI;

    //search window and functionality
    private SwingSearchWindow searchFrame;

    //command functions
    private Commands currentCommands;

    //warn user if viewer not setup fully
    private boolean isSetup;

    //tell software to exit on close - default is true
    public static boolean exitOnClose = true;

    //Throw runtime exception when user tries to open document after close() has been called
    public static boolean closeCalled;

    /**
     * setup and run client
     */
    public Viewer() {

        final String prefFile = System.getProperty("org.jpedal.Viewer.Prefs");
        if (prefFile != null) {
            properties.loadProperties(prefFile);
        else {
            properties.loadProperties();
        }

        init();

        //enable error messages which are OFF by default
        DecoderOptions.showErrorMessages = true;

    }

    /**
     * setup and run client passing in parameter that points to the preferences file we should use.
     * preferences file
     *
     @param rootContainer   Is a swing component that the viewer is displayed inside
     @param preferencesPath The path of the preferences file
     */
    public Viewer(final javax.accessibility.Accessible rootContainer, final String preferencesPath) {

        if (preferencesPath != null && !preferencesPath.isEmpty()) {
            try {
                properties.loadProperties(preferencesPath);
            catch (final Exception e) {
                System.err.println("Specified Preferrences file not found at " + preferencesPath + ". If this file is within a jar ensure filename has jar: at the begining.\n\nLoading default properties. " + e);

                properties.loadProperties();
            }
        else {
            properties.loadProperties();
        }
        init();

        //enable error messages which are OFF by default
        DecoderOptions.showErrorMessages = true;

        setRootContainer(rootContainer);

    }

    /**
     * setup and run client passing in paramter that points to the preferences file we should use.
     *
     @param prefs Full path to xml file containing preferences
     */
    public Viewer(final String prefs) {
        try {
            properties.loadProperties(prefs);
        catch (final Exception e) {
            System.err.println("Specified Preferrences file not found at " + prefs + ". If this file is within a jar ensure filename has jar: at the begining.\n\nLoading default properties. " + e);

            properties.loadProperties();
        }
        init();

        //enable error messages which are OFF by default
        DecoderOptions.showErrorMessages = true;

    }


    private void init() {

        //switch on if not set by user
        if (System.getProperty("org.jpedal.viewerLargeImageCaching"== null) {
            System.setProperty("org.jpedal.viewerLargeImageCaching""true");
        }

        decode_pdf = new PdfDecoder(true);

        thumbnails = new SwingThumbnailPanel(decode_pdf);

        currentGUI = new SwingGUI(decode_pdf, commonValues, thumbnails, properties);

        decode_pdf.addExternalHandler(new DefaultActionHandler(currentGUI), Options.FormsActionHandler);
        decode_pdf.addExternalHandler(new SwingClientExternalHandler(), Options.AdditionalHandler);

        searchFrame = new SwingSearchWindow(currentGUI);

        currentCommands = new Commands(commonValues, currentGUI, decode_pdf,
                thumbnails, properties, searchFrame);

    }

    /**
     * Get the search results from a search performing in the Viewer.
     * This is how users searching from a Viewer object can retrieve results
     * if they wish to use them outside of the Viewer.
     *
     @return SearchList object containing the current search results list
     */
    @SuppressWarnings("unused")
    public SearchList getSearchResults() {
        return currentCommands.getSearchList();
    }

    /**
     * main method to run the software as standalone application
     *
     @param args Program arguments passed into the Viewer.
     */
    @SuppressWarnings("unused")
    public static void main(final String[] args) {

        final Viewer current = new Viewer();
        current.setupViewer();
        current.handleArguments(args);
    }

    /**
     * open the file passed in by user on startup (do not call directly)
     * USED by our tests
     */
    public SwingGUI getSwingGUI() {
        return currentGUI;
    }


    /**
     @param defaultFile Allow user to open PDF file to display
     */
    public void openDefaultFile(final String defaultFile) {

        //reset flag
        if (thumbnails.isShownOnscreen()) {
            thumbnails.resetToDefault();
        }

        commonValues.maxViewY = 0// Ensure reset for any viewport

        // Open any default file and selected page
        if (defaultFile != null) {

            final File testExists = new File(defaultFile);
            boolean isURL = false;
            if (defaultFile.startsWith("http:"|| defaultFile.startsWith("jar:"|| defaultFile.startsWith("file:")) {
                LogWriter.writeLog("Opening http connection");
                isURL = true;
            }

            if ((!isURL&& (!testExists.exists())) {
                currentGUI.showMessageDialog(defaultFile + '\n' + Messages.getMessage("PdfViewerdoesNotExist.message"));
            else if ((!isURL&& (testExists.isDirectory())) {
                currentGUI.showMessageDialog(defaultFile + '\n' + Messages.getMessage("PdfViewerFileIsDirectory.message"));
            else {
                commonValues.setFileSize(testExists.length() >> 10);

                commonValues.setSelectedFile(defaultFile);

                currentGUI.setViewerTitle(null);

                //see if user set Page
                final String page = System.getProperty("org.jpedal.page");
                final String bookmark = System.getProperty("org.jpedal.bookmark");
                if (page != null && !isURL) {

                    try {
                        int pageNum = Integer.parseInt(page);

                        if (pageNum < 1) {
                            pageNum = -1;
                            System.err.println(page + " must be 1 or larger. Opening on page 1");
                            LogWriter.writeLog(page + " must be 1 or larger. Opening on page 1");
                        }

                        if (pageNum != -1) {
                            openFile(testExists, pageNum);
                        }


                    catch (final NumberFormatException e) {
                        System.err.println(page + "is not a valid number for a page number. Opening on page 1 " + e);
                        LogWriter.writeLog(page + "is not a valid number for a page number. Opening on page 1");
                    }
                else if (bookmark != null) {
                    openFile(testExists, bookmark);
                else {
                    currentGUI.openFile(defaultFile);
                }
            }
        }
    }

    /**
     @param defaultFile Allow user to open PDF file to display
     */
    private void openDefaultFileAtPage(final String defaultFile, final int page) {

        //reset flag
        if (thumbnails.isShownOnscreen()) {
            thumbnails.resetToDefault();
        }

        commonValues.maxViewY = 0// Ensure reset for any viewport

        //open any default file and selected page
        if (defaultFile != null) {

            final File testExists = new File(defaultFile);
            boolean isURL = false;
            if (defaultFile.startsWith("http:"|| defaultFile.startsWith("jar:")) {
                LogWriter.writeLog("Opening http connection");
                isURL = true;
            }

            if ((!isURL&& (!testExists.exists())) {
                currentGUI.showMessageDialog(defaultFile + '\n' + Messages.getMessage("PdfViewerdoesNotExist.message"));
            else if ((!isURL&& (testExists.isDirectory())) {
                currentGUI.showMessageDialog(defaultFile + '\n' + Messages.getMessage("PdfViewerFileIsDirectory.message"));
            else {

                commonValues.setSelectedFile(defaultFile);
                commonValues.setFileSize(testExists.length() >> 10);
                currentGUI.setViewerTitle(null);

                openFile(testExists, page);

            }
        }
    }

    public PdfDecoderInt getPdfDecoder() {
        return decode_pdf;
    }

    public void setRootContainer(final Object rootContainer) {
        currentGUI.setRootContainer(rootContainer);
    }

    /**
     * Should be called before setupViewer
     */
    public void loadProperties(final String props) {
        properties.loadProperties(props);
    }

    /**
     * initialise and run client (default as Application in own Frame)
     */
    public void setupViewer() {

        //also allow messages to be suppressed with JVM option
        final String flag = System.getProperty("org.jpedal.suppressViewerPopups");
        boolean suppressViewerPopups = false;

        if (flag != null && flag.equalsIgnoreCase("true")) {
            suppressViewerPopups = true;
        }

        //set search window position here to ensure that gui has correct value
        final String searchType = properties.getValue("searchWindowType");
        if (searchType != null && !searchType.isEmpty()) {
            final int type = Integer.parseInt(searchType);
            searchFrame.setViewStyle(type);
        else {
            searchFrame.setViewStyle(SwingSearchWindow.SEARCH_MENU_BAR);
        }

        searchFrame.setUpdateListDuringSearch(properties.getValue("updateResultsDuringSearch").equals("true"));

        //Set search frame here
        currentGUI.setSearchFrame(searchFrame);

        //switch on thumbnails if flag set
        final String setThumbnail = System.getProperty("org.jpedal.thumbnail");
        if (setThumbnail != null) {
            if (setThumbnail.equals("true")) {
                thumbnails.setThumbnailsEnabled(true);
            else if (setThumbnail.equals("false")) {
                thumbnails.setThumbnailsEnabled(false);
            }
        else //default
            thumbnails.setThumbnailsEnabled(true);
        }

        //allow user to override messages
                 /*
         * allow user to define country and language settings
         *
         * you will need a file called messages_XX.properties in
         * org.jpedal.international.messages where XX is a valid Locale.
         *
         * You can also choose an alternative Lovation - see sample code below
         *
         *  You can manually set Java to use a Locale with this code
         *  (also useful to test)
         *
         *    Example here is Brazil (note no Locale files present for it)
         *
         * If you make and Locale files, we would be delighted to include them
         * in future versions of the software.
         *
         java.util.Locale aLocale = new java.util.Locale("br", "BR");

         java.util.Locale.setDefault(aLocale);
         */


        final String customBundle = System.getProperty("org.jpedal.bundleLocation");
        final ResourceBundle bundle;

        if (customBundle != null) {

            final ClassLoader loader = Messages.class.getClassLoader();
            final String fileName = customBundle.replaceAll("\\.""/"'_' + java.util.Locale.getDefault().getLanguage() ".properties";
            final URL resource = loader.getResource(fileName);

            if (resource != null) {
                bundle = ResourceBundle.getBundle(customBundle);
            else {
                java.util.Locale.setDefault(new java.util.Locale("en""EN"));
                currentGUI.showMessageDialog("No locale file " + fileName + " has been defined for this Locale - using English as Default" +
                        "\n Format is path, using '.' as break ie org.jpedal.international.messages");
                bundle = null;
            }

        else {
            bundle = null;
        }

        init(bundle);

        //gui setup, create gui, load properties
        currentGUI.init(currentCommands);

        if (searchFrame.getViewStyle() == SwingSearchWindow.SEARCH_TABBED_PANE) {
            currentGUI.searchInTab(searchFrame);
        }

        String propValue = properties.getValue("showfirsttimepopup");
        final boolean showFirstTimePopup = !suppressViewerPopups && !propValue.isEmpty() && propValue.equals("true");

        if (showFirstTimePopup) {
            currentGUI.showFirstTimePopup();
            properties.setValue("showfirsttimepopup""false");
        }

        propValue = properties.getValue("displaytipsonstartup");
        if (!suppressViewerPopups && !propValue.isEmpty() && propValue.equals("true")) {
            currentCommands.executeCommand(Commands.TIP, null);
        }

        //flag so we can warn user if they call executeCommand without it setup
        isSetup = true;
    }

    /**
     * setup the viewer
     */
    private void init(final ResourceBundle bundle) {

        //load correct set of messages
        if (bundle == null) {

            //load locale file
            try {
                Messages.setBundle(ResourceBundle.getBundle("org.jpedal.international.messages"));
            catch (final Exception e) {

                e.printStackTrace();

                LogWriter.writeLog("Exception " + e + " loading resource bundle.\n" +
                        "Also check you have a file in org.jpedal.international.messages to support Locale=" + java.util.Locale.getDefault());
            }

        else {
            try {
                Messages.setBundle(bundle);
            catch (final Exception ee) {
                LogWriter.writeLog("Exception with bundle " + bundle);
                ee.printStackTrace();
            }
        }

        //pass through GUI for use in multipages and Javascript
        decode_pdf.addExternalHandler(currentGUI, Options.MultiPageUpdate);

        //used to test ability to replace Javascript with own engine
        //org.jpedal.objects.javascript.ExpressionEngine marksTest=new TestEngine();
        //decode_pdf.addExternalHandler(marksTest, Options.ExpressionEngine);

        //debugging code to create a log
        //LogWriter.setupLogFile("v");
        //LogWriter.log_name =  "/mnt/shared/log.txt";

        //make sure widths in data CRITICAL if we want to split lines correctly!!
        DecoderOptions.embedWidthData = true;

        
        /*
         * ANNOTATIONS code
         *
         * replace Annotations with your own custom annotations using paint code
         *
         */
        //decode_pdf.setAnnotationsVisible(false); //disable built-in annotations and use custom versions
        //code to create a unique iconset
        //see also
org.jpedal.examples.viewer.gui.GUI.handleAnnotations()
        //this allows the user to place fonts in the classpath and use these for display, as if embedded
        //decode_pdf.addSubstituteFonts("org/jpedal/res/fonts/", true);

        //set to extract all
        //COMMENT OUT THIS LINE IF USING JUST THE VIEWER
        decode_pdf.setExtractionMode(01)//values extraction mode,dpi of images, dpi of page as a factor of 72

        //don't extract text and images (we just want the display)

        /*
         * FONT EXAMPLE CODE showing JPedal's functionality to set values for
         * non-embedded fonts.
         *
         * This allows sophisticated substitution of non-embedded fonts.
         *
         * Most font mapping is done as the fonts are read, so these calls must
         * be made BEFORE the openFile() call.
         */

                 /*
         * FONT EXAMPLE - Replace global default for non-embedded fonts.
         *
         * You can replace Lucida as the standard font used for all non-embedded and substituted fonts
         * by using is code.
         * Java fonts are case sensitive, but JPedal resolves currentGUI.frame, so you could
         * use Webdings, webdings or webDings for Java font Webdings
         */

        /* Removed to save time on startup - uncomment if it causes problems
         try{
         //choice of example font to stand-out (useful in checking results to ensure no font missed.
         //In general use Helvetica or similar is recommended
         //            decode_pdf.setDefaultDisplayFont("SansSerif");
         }catch(PdfFontException e){ //if its not available catch error and show valid list

         System.out.println(e.getMessage());

         //get list of fonts you can use
         String[] fontList =GraphicsEnvironment.getLocalGraphicsEnvironment().getAvailableFontFamilyNames();
         System.out.println(Messages.getMessage("PdfViewerFontsFound.message"));
         System.out.println("=====================\n");
         int count = fontList.length;
         for (int i = 0; i < count; i++) {
         Font f=new Font(fontList[i],1,10);
         System.out.println(fontList[i]+" ("+Messages.getMessage("PdfViewerFontsPostscript.message")+ '=' +f.getPSName()+ ')');

         }
         System.exit(1);

         }/**/

        /*
         * IMPORTANT note on fonts for EXAMPLES
         *
         * USEFUL TIP : The Viewer displays a list of fonts used on the
         * current PDF page with the File > Fonts menu option.
         *
         * PDF allows the use of weights for fonts so Arial,Bold is a weight of
         * Arial. This value is not case sensitive so JPedal would regard
         * arial,bold and aRiaL,BoLd as the same.
         *
         * Java supports a set of Font families internally (which may have
         * weights), while JPedals substitution facility uses physical True Type
         * fonts so it is resolving each font weight separately. So mapping
         * works differently, depending on which is being used.
         *
         * If you are using a font, which is named as arial,bold you can use
         * either arial,bold or arial (and JPedal will then try to select the
         * bold weight if a Java font is used).
         *
         * So for a font such as Arial,Bold JPedal will test for an external
         * truetype font substitution (ie arialMT.ttf) mapped to Arial,Bold. BUT
         * if the substitute font is a Java font an additional test will be made
         * for a match against Arial if there is no match on Arial,Bold.
         *
         * If you want to map all Arial to equivalents to a Java font such as
         * Times New Roman, just map Arial to Times New Roman (only works for
         * inbuilt java fonts). Note if you map Arial,Bold to a Java font such
         * as Times New Roman, you will get Times New Roman in a bold weight, if
         * available. You cannot set a weight for the Java font.
         *
         * If you wish to substitute Arial but not Arial,Bold you should
         * explicitly map Arial,Bold to Arial,Bold as well.
         *
         * The reason for the difference is that when using Javas inbuilt fonts
         * JPedal can resolve the Font Family and will try to work out the
         * weight internally. When substituting Truetype fonts, these only
         * contain ONE weight so JPedal is resolving the Font and any weight as
         * a separate font . Different weights will require separate files.
         *
         */

        
        /*
         * FONT EXAMPLE - Use fonts located on machine for substitution
         *
         * This code explains how to use JPedal to substitute fonts which are
         * not embedded using fonts held in any font directory.
         *
         * It works as follows:-
         *
         * If the -Dorg.jpedal.fontdirs="C:/win/fonts/","/mnt/X11/fonts" is set to a
         * comma-separated list of directories, any truetype fonts (with .ttf
         * file ending) will be logged and added to the substitution table. So
         * arialMT.ttf will be added as arialmt. If arialmt is used in the PDF
         * but not embedded, JPedal will use this font file to render it.
         *
         * If a command line paramter is not appropriate, the call
         * setFontDirs(String[] fontDirs) will achieve the same.
         *
         *
         * If the name is not an exact match (ie you have arialMT which you wish
         * to use to display arial, you can use the method
         * setSubstitutedFontAliases(String[] name, String[] aliases) to convert
         * it internally - see sample code at bottom of note.
         *
         * The Name is not case-sensitive.
         *
         * Spaces are important so TimesNewRoman and Times New Roman are
         * degarded as 2 fonts.
         *
         * If you have 2 copies of arialMT.ttf in the scanned directories, the
         * last one will be used.
         *
         * If the file was called arialMT,bold.ttf it is resolved as
         * ArialMT,bold only.
         *
         */

        //mappings for non-embedded fonts to use
        FontMappings.setFontReplacements();

        //decode_pdf.setFontDirs(new String[]{"C:/windows/fonts/","C:/winNT/fonts/"});
        /*
         * FONT EXAMPLE - Use Standard Java fonts for substitution
         *
         * This code tells JPedal to substitute fonts which are not embedded.
         *
         * The Name is not case-sensitive.
         *
         * Spaces are important so TimesNewRoman and Times New Roman are
         * degarded as 2 fonts.
         *
         * If you have 2 copies of arialMT.ttf in the scanned directories, the
         * last one will be used.
         *
         *
         * If you wish to use one of Javas fonts for display (for example, Times
         * New Roman is a close match for myCompanyFont in the PDF, you can the
         * code below
         *
         * String[] aliases={"Times New Roman"};//,"helvetica","arial"};
         * decode_pdf.setSubstitutedFontAliases("myCompanyFont",aliases);
         *
         * Here is is used to map Javas Times New Roman (and all weights) to
         * TimesNewRoman.
         *
         * This can also be done with the command -org.jpedal.fontmaps="TimesNewRoman=Times New Roman","font2=pdfFont1"
         */
        //String[] nameInPDF={"TimesNewRoman"};//,"helvetica","arial"};
        //decode_pdf.setSubstitutedFontAliases("Times New Roman",nameInPDF);

        
        /*
         * add in external handlers for code - 2 examples supplied
         *

         //org.jpedal.external.ImageHandler myExampleImageHandler=new org.jpedal.examples.handlers.ExampleImageDecodeHandler();
         org.jpedal.external.ImageHandler myExampleImageHandler=new org.jpedal.examples.handlers.ExampleImageDrawOnScreenHandler();

         decode_pdf.addExternalHandler(myExampleImageHandler, Options.ImageHandler);


         /**/
        /*
         * divert all message to our custom code
         *

         CustomMessageHandler myExampleCustomMessageHandler =new ExampleCustomMessageHandler();

         decode_pdf.addExternalHandler(myExampleCustomMessageHandler, Options.CustomMessageOutput);

         /**/

    }

    /**
     * Have the viewer handle program arguments
     *
     @param args :: Program arguments passed into the Viewer.
     */
    public void handleArguments(final String[] args) {

        //Ensure default open is on event thread, otherwise the display is updated as values are changing
        if (SwingUtilities.isEventDispatchThread()) {

            if (args.length > 0) {
                openDefaultFile(args[0]);
            else if ((properties.getValue("openLastDocument").equalsIgnoreCase("true"))
                    && (properties.getRecentDocuments() != null
                    && properties.getRecentDocuments().length > 1)) {

                int lastPageViewed = Integer.parseInt(properties.getValue("lastDocumentPage"));

                if (lastPageViewed < 0) {
                    lastPageViewed = 1;
                }

                openDefaultFileAtPage(properties.getRecentDocuments()[0], lastPageViewed);
            }
        else {

            final Runnable run = () -> {
                if (args.length > 0) {
                    openDefaultFile(args[0]);

                else if (properties.getValue("openLastDocument").toLowerCase().equals("true")) {
                    if (properties.getRecentDocuments() != null
                            && properties.getRecentDocuments().length > 1) {

                        int lastPageViewed = Integer.parseInt(properties.getValue("lastDocumentPage"));

                        if (lastPageViewed < 0) {
                            lastPageViewed = 1;
                        }

                        openDefaultFileAtPage(properties.getRecentDocuments()[0], lastPageViewed);
                    }
                }
            };
            SwingUtilities.invokeLater(run);
        }
    }

    /**
     * General code to open file at specified boomark - do not call directly
     *
     @param file     File the PDF to be decoded
     @param bookmark - if not present, exception will be thrown
     */
    private void openFile(final File file, final String bookmark) {

        try {
            final boolean fileCanBeOpened = OpenFile.openUpFile(file.getCanonicalPath(), commonValues, searchFrame, currentGUI, decode_pdf, properties, thumbnails);

            String bookmarkPage = null;

            int page = -1;

            //reads tree and populates lookup table
            if (decode_pdf.getOutlineAsXML() != null) {
                final Node rootNode = decode_pdf.getOutlineAsXML().getFirstChild();
                if (rootNode != null) {
                    bookmarkPage = currentGUI.getBookmark(bookmark);
                }

                if (bookmarkPage != null) {
                    page = Integer.parseInt(bookmarkPage);
                }
            }

            //it may be a named destination ( ie bookmark=Test1)
            if (bookmarkPage == null) {
                bookmarkPage = decode_pdf.getIO().convertNameToRef(bookmark);

                if (bookmarkPage != null) {

                    //read the object
                    final PdfObject namedDest = new OutlineObject(bookmarkPage);
                    decode_pdf.getIO().readObject(namedDest);

                    //still needed to init viewer
                    if (fileCanBeOpened) {
                        OpenFile.processPage(commonValues, decode_pdf, currentGUI, thumbnails);
                    }

                    //and generic open Dest code
                    decode_pdf.getFormRenderer().getActionHandler().gotoDest(namedDest, ActionHandler.MOUSECLICKED, PdfDictionary.Dest);
                }
            }

            if (bookmarkPage == null) {
                throw new PdfException("Unknown bookmark " + bookmark);
            }


            if (page > -1) {
                commonValues.setCurrentPage(page);
                if (fileCanBeOpened) {
                    OpenFile.processPage(commonValues, decode_pdf, currentGUI, thumbnails);
                }
            }
        catch (final IOException | PdfException | NumberFormatException e) {
            System.err.println("Exception " + e + " processing file");

            e.printStackTrace();

            Values.setProcessing(false);
        }
    }

    /**
     * General code to open file at specified page - do not call directly
     *
     @param file File the PDF to be decoded
     @param page int page number to show the user
     */
    private void openFile(final File file, final int page) {

        try {
            final boolean fileCanBeOpened = OpenFile.openUpFile(file.getCanonicalPath(), commonValues, searchFrame, currentGUI, decode_pdf, properties, thumbnails);

            commonValues.setCurrentPage(page);

            if (fileCanBeOpened) {
                OpenFile.processPage(commonValues, decode_pdf, currentGUI, thumbnails);
            }
        catch (final IOException | PdfException e) {
            System.err.println("Exception " + e + " processing file");

            e.printStackTrace();

            Values.setProcessing(false);
        }
    }

    /**
     * Execute Jpedal functionality from outside of the library using this method.
     * EXAMPLES
     * commandID = Commands.OPENFILE, args = {"/PDFData/Hand_Test/crbtrader.pdf}"
     * commandID = Commands.OPENFILE, args = {byte[] = {0,1,1,0,1,1,1,0,0,1}, "/PDFData/Hand_Test/crbtrader.pdf}"
     * commandID = Commands.ROTATION, args = {"90"}
     * commandID = Commands.OPENURL,  args = {"http://www.cs.bham.ac.uk/~axj/pub/papers/handy1.pdf"}
     <p>
     * for full details see http://www.idrsolutions.com/access-pdf-viewer-features-from-your-code/
     *
     @param commandID :: static int value from Commands to spedify which command is wanted
     @param args      :: arguements for the desired command
     */
    @SuppressWarnings("UnusedReturnValue")
    public Object executeCommand(final int commandID, final Object[] args) {

        //far too easy to miss this step (I did!) so warn user
        if (!isSetup) {
            throw new RuntimeException("You must call viewer.setupViewer(); before you call any commands");
        }

        return currentCommands.executeCommand(commandID, args);

    }

    /**
     * Allows external helper classes to be added to JPedal to alter default functionality.
     <br><br>If Options.FormsActionHandler is the type then the <b>newHandler</b> should be
     * of the form <b>org.jpedal.objects.acroforms.ActionHandler</b>
     <br><br>If Options.JPedalActionHandler is the type then the <b>newHandler</b> should be
     * of the form <b>Map</b> which contains Command Integers, mapped onto their respective
     <b>org.jpedal.examples.viewer.gui.swing.JPedalActionHandler</b> implementations.  For example,
     * to create a custom help action, you would add to your map, Integer(Commands.HELP) -&gt;  JPedalActionHandler.
     * For a tutorial on creating custom actions in the Viewer, see
     <b>https://support.idrsolutions.com/hc/en-us/articles/115002795272-Custom-Interfaces</b>
     *
     @param newHandler Implementation of interface provided by IDR solutions
     @param type       Defined value into org.jpedal.external.Options class
     */
    public void addExternalHandler(final java.util.Map<Integer, Object> newHandler, final int type) {
        decode_pdf.addExternalHandler(newHandler, type);
    }

    /**
     * run with caution and only at end of usage if you really need
     */
    public void dispose() {

        commonValues = null;

        if (thumbnails != null) {
            thumbnails.dispose();
        }

        thumbnails = null;

        if (properties != null) {
            properties.dispose();
        }

        properties = null;

        if (currentGUI != null) {
            currentGUI.dispose();
        }

        currentGUI = null;

        searchFrame = null;

        currentCommands = null;

        if (decode_pdf != null) {
            decode_pdf.dispose();
        }

        decode_pdf = null;

        Messages.dispose();

    }
}