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

import com.formdev.flatlaf.FlatDarculaLaf;
import com.formdev.flatlaf.FlatDarkLaf;
import com.formdev.flatlaf.FlatIntelliJLaf;
import com.formdev.flatlaf.FlatLightLaf;
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.grouping.PdfGroupingAlgorithms;
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 javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.net.URL;
import java.util.Arrays;
import java.util.ResourceBundle;


/**
 <h2><b>PDF Viewer</b></h2>
 *
 <p><b>Run directly from jar with java -cp jpedal.jar org/jpedal/examples/viewer/Viewer</b></p>
 *
 <p>There are plenty of tutorials on how to configure the Viewer on our website
 * <a href="https://support.idrsolutions.com/jpedal/tutorials/viewer/">Support Section.</a></p>
 *
 <p>If you want to implement your own there are tutorials at :
 * <a href="https://support.idrsolutions.com/jpedal/tutorials/optional/">Customizing the Viewer</a></p>
 <p>We recommend you look at the full viewer as it is totally configurable and does everything for you.</p>
 *
 *
 <p>Fully featured GUI viewer and demonstration of JPedal's capabilities.</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 {

    static {
        FlatDarkLaf.installLafInfo();
        FlatDarculaLaf.installLafInfo();
        FlatIntelliJLaf.installLafInfo();
        FlatLightLaf.installLafInfo();
    }

    //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
    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;

    }

    /**
     * setup and run client
     *
     @param enableDebugMode Run the client in debug mode
     */
    @SuppressWarnings("SameParameterValue")
    private Viewer(final boolean enableDebugMode) {
        if (enableDebugMode) {
            //Disable most menu options
            properties.loadDebugProperties();
        else {
            final String prefFile = System.getProperty("org.jpedal.Viewer.Prefs");
            if (prefFile != null) {
                properties.loadProperties(prefFile);
            else {
                properties.loadProperties();
            }
        }
        init();

        //Allow debug menu items to appear
        currentGUI.setDebugMode(enableDebugMode);

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

    private void init() {

        // Make sure this is called before initialising the components
        String lafOption = System.getProperty("org.jpedal.userControlledLAF");
        //checking that the using is using the correctly spelled version of the flag
        final String altLafOption = System.getProperty("org.jpedal.userControledLAF");
        if (altLafOption != null && !altLafOption.isEmpty()) {
            lafOption = altLafOption;
        }
        if (lafOption != null && !lafOption.isEmpty()) {
            if (!"true".equalsIgnoreCase(lafOption)) {
                if ("false".equalsIgnoreCase(lafOption)) {
                    setLookAndFeel();
                else {
                    try {
                        UIManager.setLookAndFeel(lafOption);
                    catch (final ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException ex) {
                        currentGUI.showMessageDialog("Exception " + ex + " could not be found. LookAndFeel not set.");
                    }
                }
            }
        else {
            final String laf = properties.getValue("viewerLookAndFeel");
            if (!laf.isEmpty()) {

                if ("SystemDefault".equals(laf|| !Arrays.toString(UIManager.getInstalledLookAndFeels()).contains(laf)) {
                    setLookAndFeel();
                else {
                    try {
                        UIManager.setLookAndFeel(laf);
                    catch (final ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException ex) {
                        LogWriter.writeLog("Exception " + ex + " setting look and feel to viewerLookAndFeel value");
                    }
                }
            }
        }

        //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);

    }

    /**
     * set the look and feel for the GUI components to be the
     * default for the system it is running on
     */
    private static void setLookAndFeel() {
        try {
            UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
        catch (final ClassNotFoundException | IllegalAccessException | InstantiationException | UnsupportedLookAndFeelException e) {
            LogWriter.writeLog("Exception " + e + " setting look and feel");
        }
    }

    /**
     * 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);
    }

    /**
     * run the viewer in debug mode
     *
     @param args Program arguments passed into the Viewer
     */
    public static void debug(final String[] args) {
        final Viewer current = new Viewer(true);
        current.setupViewer();
        current.handleArguments(args);
    }

    /**
     * open the file passed in by user on startup (do not call directly)
     * USED by our tests
     *
     @return the current gui
     */
    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();

                //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();

                openFile(testExists, page);

            }
        }
    }

    public PdfDecoderInt getPdfDecoder() {
        return decode_pdf;
    }

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

    /**
     * Should be called before setupViewer
     *
     @param props path to the properties file to load
     */
    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 ("true".equalsIgnoreCase(flag)) {
            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("true".equals(properties.getValue("updateResultsDuringSearch")));

        //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 ("true".equals(setThumbnail)) {
                thumbnails.setThumbnailsEnabled(true);
            else if ("false".equals(setThumbnail)) {
                thumbnails.setThumbnailsEnabled(false);
            }
        else //default
            thumbnails.setThumbnailsEnabled(true);
        }

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

        if (customBundle != null) {

            final String fileName = customBundle.replaceAll("\\.""/"'_' + java.util.Locale.getDefault().getLanguage() ".properties";
            final URL resource = Messages.class.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() && "true".equals(propValue);

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

        propValue = properties.getValue("displaytipsonstartup");
        if (!suppressViewerPopups && !propValue.isEmpty() && "true".equals(propValue)) {
            currentCommands.executeCommand(ViewerCommands.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) {

                LogWriter.writeLog(e);

                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);
                LogWriter.writeLog(ee);
            }
        }

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

        //make sure widths in data CRITICAL if we want to split lines correctly!!
        DecoderOptions.embedWidthData = 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

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

    }

    /**
     * Have the viewer handle program arguments
     *
     @param args :: Program arguments passed into the Viewer.
     */
    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 (("true".equalsIgnoreCase(properties.getValue("openLastDocument")))
                    && (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 ("true".equalsIgnoreCase(properties.getValue("openLastDocument"))
                        && properties.getRecentDocuments() != null
                        && properties.getRecentDocuments().length > 1) {

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

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

                    openDefaultFileAtPage(properties.getRecentDocuments()[0], lastPageViewed);
                }
            };
            try {
                SwingUtilities.invokeAndWait(run);
            catch (InterruptedException | InvocationTargetException e) {
                LogWriter.writeLog("Exception: " + e.getMessage());
            }
        }
    }

    /**
     * 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) {
            LogWriter.writeLog(e);

            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) {
            LogWriter.writeLog(e);

            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 https://support.idrsolutions.com/jpedal/tutorials/viewer/access-pdf-viewer-features-from-your-code
     *
     @deprecated use {@link #executeCommand(ViewerCommands, Object[])} instead.
     *
     @param commandID :: static int value from Commands to specify which command is wanted
     @param args      :: arguments for the desired command
     @return any returned value from executed command
     */
    @Deprecated
    @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");
        }

        final ViewerCommands command = ViewerCommands.createFromID(commandID);
        if (command != null) {
            return currentCommands.executeCommand(command, args);
        else {
            LogWriter.writeLog("Command for ID value " + commandID + " does not exist.");
            return null;
        }

    }

    /**
     * Execute Jpedal functionality from outside of the library using this method.
     * EXAMPLES
     * command = Command.OPENFILE, args = {"/PDFData/Hand_Test/crbtrader.pdf}"
     * command = Command.OPENFILE, args = {byte[] = {0,1,1,0,1,1,1,0,0,1}, "/PDFData/Hand_Test/crbtrader.pdf}"
     * command = Command.ROTATION, args = {"90"}
     * command = Command.OPENURL,  args = {"http://www.cs.bham.ac.uk/~axj/pub/papers/handy1.pdf"}
     <p>
     * for full details see https://support.idrsolutions.com/jpedal/tutorials/viewer/access-pdf-viewer-features-from-your-code
     *
     @param command :: enum value from ViewerCommands to specify which command is wanted
     @param args    :: arguments for the desired command
     @return any returned value from executed command
     */
    public Object executeCommand(final ViewerCommands command, final Object... args) {
        if (!isSetup) {
            throw new RuntimeException("You must call viewer.setupViewer(); before you call any commands");
        }

        return currentCommands.executeCommand(command, args);
    }

    /**
     * Query the Viewer for the values it holds.
     @param value :: enum value from ViewerValues to specify which value is wanted
     @return any returned value or null
     */
    public Object getViewerValue(final ViewerValues value) {
        if (!isSetup) {
            throw new RuntimeException("You must call viewer.setupViewer(); before query and values");
        }

        return currentCommands.getValue(value);
    }

    /**
     * Get the outline panel.
     @return the returned object is a SwingOutline panel
     */
    public Object getOutlinePanel() {
        return currentGUI.getOutlinePanel();
    }

    /**
     * Get the thumbnail panel.
     @return the returned object is a SwingThumbnailPanel
     */
    public SwingThumbnailPanel getThumbnailPanel() {
        return currentGUI.getThumbnailPanel();
    }

    /**
     * Get the page grouping.
     @param pageNumber the page number
     @return the PdfGrouping object for the page to access text content
     */
    public Object getPageGrouping(final int pageNumber) {

        PdfGroupingAlgorithms grouping = null;
        if (pageNumber == decode_pdf.getlastPageDecoded()) {
            try {
                grouping = decode_pdf.getGroupingObject();
            catch (final PdfException e) {
                LogWriter.writeLog(e);
            }
        else {
            try {
                decode_pdf.decodePageInBackground(pageNumber);
                grouping = decode_pdf.getBackgroundGroupingObject();
            catch (final Exception e) {
                LogWriter.writeLog(e);
            }
        }

        //ensure done
        decode_pdf.waitForDecodingToFinish();

        return grouping;
    }

    /**
     * Get the page counter
     @return the returned object is a JTextField
     */
    public Object getPageCounter() {
        return currentGUI.getPageCounter(SwingGUI.PageCounter.PAGECOUNTER2);
    }

    /**
     * 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/jpedal/api-documents/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();

    }
}