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

import org.jpedal.PdfDecoderInt;
import org.jpedal.display.Display;
import org.jpedal.display.GUIDisplay;
import org.jpedal.examples.viewer.Commands;
import org.jpedal.examples.viewer.Values;
import org.jpedal.examples.viewer.commands.NextResults;
import org.jpedal.examples.viewer.commands.PageNavigator;
import org.jpedal.examples.viewer.commands.Scroll;
import org.jpedal.examples.viewer.gui.SwingGUI;
import org.jpedal.external.JPedalActionHandler;
import org.jpedal.external.Options;
import org.jpedal.grouping.DefaultSearchListener;
import org.jpedal.grouping.PdfGroupingAlgorithms;
import org.jpedal.grouping.SearchType;
import org.jpedal.objects.PdfPageData;
import org.jpedal.utils.LogWriter;
import org.jpedal.utils.Messages;
import org.jpedal.utils.repositories.generic.Vector_Rectangle_Int;

import javax.swing.BorderFactory;
import javax.swing.DefaultListModel;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JComboBox;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextField;
import javax.swing.ListSelectionModel;
import javax.swing.SwingUtilities;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import javax.swing.text.JTextComponent;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Container;
import java.awt.Cursor;
import java.awt.Dimension;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.awt.Toolkit;
import java.awt.event.ActionListener;
import java.awt.event.ComponentListener;
import java.awt.event.FocusAdapter;
import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.awt.event.WindowListener;
import java.beans.PropertyChangeListener;
import java.lang.reflect.InvocationTargetException;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.SortedMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;


/**
 * Provides interactive search Window and search capabilities.
 <p>
 * This class extends JFrame to allow the option to display the search functionality
 * as an external window to the viewer. There are other ways to display the search
 * interface using the method setViewStyle(int style);
 <p>
 * For instance the search interface can be displayed as follows.
 * setViewStyle(SwingSearchWindow.SEARCH_EXTERNAL_WINDOW); // Show search interface as external window
 * setViewStyle(SwingSearchWindow.SEARCH_TABBED_PANE); // Show search interface on side tab bar
 * setViewStyle(SwingSearchWindow.SEARCH_MENU_BAR); // Show search interface on button tool bar
 */
public class SwingSearchWindow extends JFrame {

    public static final int SEARCH_EXTERNAL_WINDOW = 0;
    public static final int SEARCH_TABBED_PANE = 1;
    public static final int SEARCH_MENU_BAR = 2;

    //Flag to show search has happened and needs reset
    private boolean hasSearched;

    private Map<Integer, int[][]> searchAreas;
    private boolean backGroundSearch;
    private int searchKey;
    private final ExecutorService service = Executors.newCachedThreadPool();
    private Future<?> searchFuture;
    private int style;
    private boolean isSetup; //flag to stop multiple listeners
    private boolean usingMenuBarSearch;
    private int lastPage = -1;
    private String defaultMessage = "Search PDF Here";
    private JTextField searchText;
    private JTextField searchCount;
    private DefaultListModel<String> listModel;
    private SearchList resultsList;
    private JLabel label;
    private JPanel advancedPanel;
    private JComboBox<String> searchType;
    private JCheckBox wholeWordsOnlyBox, caseSensitiveBox, multiLineBox, highlightAll, searchAll, useRegEx, searchHighlightedOnly, ignoreWhiteSpace;
    private boolean updateListDuringSearch = true;
    private JButton searchButton;
    private int itemFoundCount;
    private String[] searchTerms = {""};
    private String lastSearchTerms;
    private boolean singlePageSearch; //Search this page only
    private Values commonValues;
    private PdfDecoderInt decode_pdf;
    private int searchTypeParameters;
    private int firstPageWithResults;
    private boolean deleteOnClick; //deletes message when user starts typing

    private final Map<Integer, Integer> textPages = new HashMap<>()//used when finding text to highlight on page
    private final Map<Integer, Object> textRectangles = new HashMap<>();
    private final JPanel nav = new JPanel();
    private final SwingGUI currentGUI;

    private final Runnable searchRunner = new Runnable() {

        @Override
        public void run() {
            itemFoundCount = 0;
            resultsList.setSelectedIndex(-1)//Ensure nothing selected so first result is highlighted correctly.
            Thread.currentThread().setName("SwingSearchWindow");
            clearCurrentResults();
            final long startTime = System.currentTimeMillis();

            final boolean searchingInBackground = backGroundSearch;

            //Now local variable is set we can turn off global variable
            backGroundSearch = false;

            final int currentKey = searchKey;
            try {
                // [AWI]: Create a new list model to append the search results to
                // NOTE: This was added to prevent a deadlock issue that occurred on the
                // EDT when the search resulted in a large number of hits
                final DefaultListModel<String> resultListModel;

                if (updateListDuringSearch) {
                    resultListModel = listModel;
                else {
                    resultListModel = new DefaultListModel<>();
                }

                int start = 1;
                int end = decode_pdf.getPageCount() 1;

                if (singlePageSearch) {
                    start = decode_pdf.getPageNumber();
                    end = start + 1;
                }

                if (!searchPages(start, end, currentKey, resultListModel, searchingInBackground)) {
                    setResultsInGUI(resultListModel, searchingInBackground, startTime);
                }

            catch (final Exception e) {
                LogWriter.writeLog("Exception in handling search " + e);

                if (currentGUI != null && currentGUI.getButtons() != null && !searchingInBackground) {
                    if (currentGUI.getButtons().getButton(Commands.NEXTRESULT!= null) {
                        currentGUI.getButtons().getButton(Commands.NEXTRESULT).setEnabled(false);
                    }
                    if (currentGUI.getButtons().getButton(Commands.PREVIOUSRESULT!= null) {
                        currentGUI.getButtons().getButton(Commands.PREVIOUSRESULT).setEnabled(false);
                    }
                }

            finally {
                commonValues.setIsSearching(false);
            }
        }


        private void setResultsInGUI(final DefaultListModel<String> resultListModel, final boolean searchingInBackground, final long searchStartTimethrows InvocationTargetException, InterruptedException {
            if (!searchingInBackground) {
                searchCount.setText(Messages.getMessage("PdfViewerSearch.ItemsFound"' ' + itemFoundCount + "  "
                        + Messages.getMessage("PdfViewerSearch.Done"));
            }

            // [AWI]: Update the list model displayed in the results list
            // NOTE: This was added here to address an EDT lock-up and contention issue
            // that can occur when a large result set is returned from a search. By
            // setting the model once at the end, we only take the hit for updating the
            // JList once.
            final int listSize = resultListModel.size();
            if (listSize > 1500) {
                resultListModel.removeRange(1500(listSize - 1));
                resultListModel.addElement("Too many results to list...");
            }
            listModel = resultListModel;
            SwingUtilities.invokeAndWait(() -> {
                if (resultsList != null && listModel != null) {
                    resultsList.setModel(listModel);
                }
            });

            currentGUI.setResults(resultsList);
            resultsList.setSelectedIndex(0);

            decode_pdf.repaint();

            //switch on buttons as soon as search produces valid results
            // [AWI] not used
            if (!searchingInBackground) {
                currentGUI.getButtons().getButton(Commands.NEXTRESULT).setEnabled(true);
                currentGUI.getButtons().getButton(Commands.PREVIOUSRESULT).setEnabled(true);
            }

            if (LogWriter.isRunningFromIDE) {
                //show time and memory usage
                System.out.println("Search memory=" ((Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory()) 1_000'K');

                System.out.println("Search time=" (((floatMath.abs(((System.currentTimeMillis() - searchStartTime100))) 10's');
            }
        }

        private boolean searchPages(final int start, final int end, final int currentKey,
                                    final DefaultListModel<String> resultListModel, final boolean searchingInBackgroundthrows Exception {
            final int currentPage = commonValues.getCurrentPage();
            for (int i = start; i != end; i++) {
                final int page;
                if (usingMenuBarSearch) {
                    //When using menu bar, break from loop if result found
                    if (resultsList.getResultCount() >= 1) {
                        break;
                    }

                    if (currentPage + (i - 1> commonValues.getPageCount()) {
                        page = currentPage + (i - 1- commonValues.getPageCount();
                    else {
                        page = currentPage + (i - 1);
                    }
                else {
                    page = i;
                }

                if (!Thread.currentThread().isInterrupted()) {
                    if (searchAreas != null) {
                        final int[][] highlights = searchAreas.get(page);
                        if (highlights != null) {
                            for (final int[] a  : highlights) {
                                //[AWI]: Update the search method to take the target list model as a parameter
                                searchPage(page, a[0], a[1], a[0+ a[2], a[1+ a[3], currentKey, resultListModel);
                            }
                        }
                    else {

                        //[AWI]: Update the search method to take the target list model as a parameter
                        searchPage(page, currentKey, resultListModel);
                    }
                else {
                    return true;
                }

                // new value or 16 pages elapsed
                if (!searchingInBackground && (resultListModel.getSize() 0|| ((page % 16== 0)) {
                    searchCount.setText(Messages.getMessage("PdfViewerSearch.ItemsFound"' ' + itemFoundCount + ' '
                            + Messages.getMessage("PdfViewerSearch.Scanning"+ page);
                }

            }
            return false;
        }

    };

    /**
     * A search interface for the viewer complete with elements to enable and disable
     * the various search options that are available.
     *
     @param currentGUI SwingGUI object used for the Viewers user interface
     */
    public SwingSearchWindow(final SwingGUI currentGUI) {
        this.currentGUI = currentGUI;
        setName("searchFrame");

        setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);

    }

    /**
     * Set if search should use the search whole words only rule
     *
     @param wholeWords true to enable whole words only rule, false to disable
     */
    public void setWholeWords(final boolean wholeWords) {
        wholeWordsOnlyBox.setSelected(wholeWords);
    }

    /**
     * Set if search should use the case sensitive search rule
     *
     @param caseSensitive true to enable case sensitive search rule, false to disable
     */
    public void setCaseSensitive(final boolean caseSensitive) {
        caseSensitiveBox.setSelected(caseSensitive);
    }

    /**
     * Set if the search should use the find multi line results rule
     *
     @param multiLine true to enable find multi line results rule, false to disable
     */
    public void setMultiLine(final boolean multiLine) {
        multiLineBox.setSelected(multiLine);
    }

    /**
     * Set if the search should ignore space characters in page content and search terms
     *
     @param ignoreSpaces true to enable ignore space character results rule, false to disable
     */
    public void setIgnoreSpaces(final boolean ignoreSpaces) {
        ignoreWhiteSpace.setSelected(ignoreSpaces);
    }

    /**
     * Set if the search result list should be updated as the search continues
     *
     @param updateListDuringSearch true to enable live list updating, false to disable
     */
    public void setUpdateListDuringSearch(final boolean updateListDuringSearch) {
        this.updateListDuringSearch = updateListDuringSearch;
    }

    /**
     * Not part of API - used internally
     <p>
     * Used to initialise the search interface ready for use.
     *
     @param dec    PdfDecoderInt object used for the current PDF
     @param values Values object used by the user interface
     */
    @SuppressWarnings({"OverlyLongMethod""java:S138"})
    public void init(final PdfDecoderInt dec, final Values values) {

        decode_pdf = dec;
        commonValues = values;

        if (isSetup) { //global variable so do NOT reinitialise
            searchCount.setText(Messages.getMessage("PdfViewerSearch.ItemsFound"' ' + itemFoundCount);
            searchText.selectAll();
            searchText.grabFocus();
        else {
            isSetup = true;

            setTitle(Messages.getMessage("PdfViewerSearchGUITitle.DefaultMessage"));

            defaultMessage = Messages.getMessage("PdfViewerSearchGUI.DefaultMessage");

            searchText = new JTextField(10);
            searchText.setText(defaultMessage);
            searchText.setName("searchText");

            createAdvancedOptionsPanel();

            nav.setLayout(new BorderLayout());

            addWindowListener(new WindowAdapter() {

                //flush objects on close
                @Override
                public void windowClosing(final WindowEvent arg0) {
                    removeSearchWindow(true);
                }
            });

            nav.add(searchButton, BorderLayout.EAST);

            nav.add(searchText, BorderLayout.CENTER);

            searchAll = new JCheckBox();
            searchAll.setSelected(true);
            searchAll.setText(Messages.getMessage("PdfViewerSearch.CheckBox"));

            final JPanel topPanel = new JPanel();
            topPanel.setLayout(new BorderLayout());
            topPanel.add(searchAll, BorderLayout.NORTH);

            label = new JLabel("<html><center> " "Show Advanced");
            final Color bgColor = label.getBackground();
            if (bgColor != null) {
                final double luma = ((0.299 * bgColor.getRed()) (0.587 * bgColor.getGreen()) (0.114 * bgColor.getBlue())) 255;
                if (luma > 0.5) {
                    label.setForeground(Color.BLUE);
                else {
                    label.setForeground(new Color(64128255));
                }
            else {
                label.setForeground(Color.BLUE);
            }

            label.setName("advSearch");

            label.addMouseListener(new MouseAdapter() {
                boolean isVisible;

                String text = "Show Advanced";

                @Override
                public void mouseEntered(final MouseEvent e) {
                    if (GUIDisplay.allowChangeCursor) {
                        nav.setCursor(new Cursor(Cursor.HAND_CURSOR));
                    }
                    label.setText("<html><center><a href=" + text + '>' + text + "</a></center>");
                }

                @Override
                public void mouseExited(final MouseEvent e) {
                    if (GUIDisplay.allowChangeCursor) {
                        nav.setCursor(new Cursor(Cursor.DEFAULT_CURSOR));
                    }
                    label.setText("<html><center>" + text);
                }

                @Override
                public void mouseClicked(final MouseEvent e) {
                    if (isVisible) {
                        text = Messages.getMessage("PdfViewerSearch.ShowOptions");
                        label.setText("<html><center><a href=" + text + '>' + text + "</a></center>");
                        advancedPanel.setVisible(false);
                    else {
                        text = Messages.getMessage("PdfViewerSearch.HideOptions");
                        label.setText("<html><center><a href=" + text + '>' + text + "</a></center>");
                        advancedPanel.setVisible(true);
                    }

                    isVisible = !isVisible;
                }
            });

            label.setBorder(BorderFactory.createEmptyBorder(3444));
            topPanel.add(label, BorderLayout.SOUTH);

            nav.add(topPanel, BorderLayout.NORTH);
            itemFoundCount = 0;
            textPages.clear();
            textRectangles.clear();
            listModel = null;

            searchCount = new JTextField(Messages.getMessage("PdfViewerSearch.ItemsFound"' ' + itemFoundCount);
            searchCount.setEditable(false);
            nav.add(searchCount, BorderLayout.SOUTH);

            listModel = new DefaultListModel<>();
            resultsList = new SearchList(listModel, textPages, textRectangles);
            resultsList.setName("results");

            addListeners();
            addComponentsToInterface();
        }

    }

    private void createAdvancedOptionsPanel() {

        /*
         * [AWI] Add a focus listener to detect when keyboard input is needed in the search text field. This
         * registration was added to support systems configured with touchscreens and virtual keyboards.
         */
        searchText.addFocusListener(new SearchTextFocusListener());

        searchButton = new JButton(Messages.getMessage("PdfViewerSearch.Button"));

        advancedPanel = new JPanel(new GridBagLayout());

        searchType = new JComboBox<>(new String[]{Messages.getMessage("PdfViewerSearch.MatchWhole"),
                Messages.getMessage("PdfViewerSearch.MatchAny")});

        wholeWordsOnlyBox = new JCheckBox(Messages.getMessage("PdfViewerSearch.WholeWords"));
        wholeWordsOnlyBox.setName("wholeWords");

        searchHighlightedOnly = new JCheckBox(Messages.getMessage("PdfViewerSearch.HighlightsOnly"));
        searchHighlightedOnly.setName("highlightsOnly");

        caseSensitiveBox = new JCheckBox(Messages.getMessage("PdfViewerSearch.CaseSense"));
        caseSensitiveBox.setName("caseSensitive");

        multiLineBox = new JCheckBox(Messages.getMessage("PdfViewerSearch.MultiLine"));
        multiLineBox.setName("multiLine");

        highlightAll = new JCheckBox(Messages.getMessage("PdfViewerSearch.HighlightsCheckBox"));
        highlightAll.setName("highlightAll");

        useRegEx = new JCheckBox(Messages.getMessage("PdfViewerSearch.RegExCheckBox"));
        useRegEx.setName("useregex");

        ignoreWhiteSpace = new JCheckBox(Messages.getMessage("PdfViewerSearch.IgnoreWhiteSpace"));
        ignoreWhiteSpace.setName("ignoreWhiteSpace");

        searchType.setName("combo");

        final GridBagConstraints c = new GridBagConstraints();

        advancedPanel.setPreferredSize(new Dimension(advancedPanel.getPreferredSize().width, 150));
        c.gridx = 0;
        c.gridy = 0;

        c.anchor = GridBagConstraints.PAGE_START;
        c.fill = GridBagConstraints.HORIZONTAL;

        c.weightx = 1;
        c.weighty = 0;
        advancedPanel.add(new JLabel(Messages.getMessage("PdfViewerSearch.ReturnResultsAs")), c);

        c.insets = new Insets(5000);
        c.gridy = 1;
        advancedPanel.add(searchType, c);

        c.gridy = 2;
        advancedPanel.add(new JLabel(Messages.getMessage("PdfViewerSearch.AdditionalOptions")), c);

        c.insets = new Insets(0000);
        c.weighty = 1;
        c.gridy = 3;
        advancedPanel.add(wholeWordsOnlyBox, c);
        c.weighty = 1;
        c.gridy = 4;
        advancedPanel.add(caseSensitiveBox, c);

        c.weighty = 1;
        c.gridy = 5;
        advancedPanel.add(multiLineBox, c);

        c.weighty = 1;
        c.gridy = 6;
        advancedPanel.add(highlightAll, c);

        c.weighty = 1;
        c.gridy = 7;
        advancedPanel.add(useRegEx, c);

        c.weighty = 1;
        c.gridy = 8;
        advancedPanel.add(searchHighlightedOnly, c);

        c.weighty = 1;
        c.gridy = 9;
        advancedPanel.add(ignoreWhiteSpace, c);

        advancedPanel.setVisible(false);

    }

    @SuppressWarnings({"OverlyLongMethod""java:S138"})
    private void addListeners() {

                 //Listener to highlight text on item selected
        resultsList.addListSelectionListener(new searchListListener());
        resultsList.setSelectionMode(ListSelectionModel.SINGLE_INTERVAL_SELECTION);

        //setup searching
        searchButton.addActionListener(e -> {

            if (!commonValues.isSearching()) {

                try {
                    searchTypeParameters = getSearchOptionsAsInt();

                    final String textToFind = searchText.getText().trim();
                    if (searchType.getSelectedIndex() == 0) { // find exact word or phrase
                        searchTerms = new String[]{textToFind};
                    else // match any of the words
                        searchTerms = textToFind.split(" ");
                        for (int i = 0; i < searchTerms.length; i++) {
                            searchTerms[i= searchTerms[i].trim();
                        }
                    }

                    singlePageSearch = !searchAll.isSelected();

                    searchText();
                catch (final Exception e1) {
                    LogWriter.writeLog(e1);
                }
            else {
                commonValues.setIsSearching(false);
                searchButton.setText(Messages.getMessage("PdfViewerSearch.Button"));
            }
            decode_pdf.requestFocus();

        });

        searchText.selectAll();
        deleteOnClick = true;

        final SwingSearchWindow finalSearchWindow = this;
        searchText.addKeyListener(new KeyAdapter() {
            @Override
            public void keyTyped(final KeyEvent e) {

                final String textToFind = searchText.getText().trim();
                if (textToFind.isEmpty()) {
                    lastSearchTerms = null;
                }

                final int id = e.getID();
                if (id == KeyEvent.KEY_TYPED) {
                    final char key = e.getKeyChar();

                    if (key == '\n') {

                        if (!decode_pdf.isOpen()) {
                            currentGUI.showMessageDialog(Messages.getMessage("PdfViewerFileMenuFind.noFile"));
                        else {
                            try {

                                commonValues.setIsSearching(false);
                                searchTypeParameters = getSearchOptionsAsInt();

                                if (textToFind.equals(lastSearchTerms)) {
                                    NextResults.execute(null, commonValues, finalSearchWindow, currentGUI, decode_pdf);
                                else {
                                    lastSearchTerms = textToFind;
                                    if (searchType.getSelectedIndex() == 0) { // find exact word or phrase
                                        searchTerms = new String[]{textToFind};
                                    else // match any of the words
                                        searchTerms = textToFind.split(" ");
                                        for (int i = 0; i < searchTerms.length; i++) {
                                            searchTerms[i= searchTerms[i].trim();
                                        }
                                    }

                                    singlePageSearch = !searchAll.isSelected();

                                    searchText();

                                    if (!usingMenuBarSearch) {
                                        decode_pdf.requestFocus();
                                    }
                                }
                            catch (final Exception e1) {
                                LogWriter.writeLog("Error performing search " + e1.getMessage());
                            }
                        }
                    }
                }
            }

            @Override
            public void keyReleased(final KeyEvent arg0) {
                if (currentGUI.getButtons().getButton(Commands.NEXTRESULT!= null) {
                    currentGUI.getButtons().getButton(Commands.NEXTRESULT).setEnabled(false);
                }
                if (currentGUI.getButtons().getButton(Commands.PREVIOUSRESULT!= null) {
                    currentGUI.getButtons().getButton(Commands.PREVIOUSRESULT).setEnabled(false);
                }
            }
        });

        searchText.addFocusListener(new FocusListener() {

            @Override
            public void focusLost(final FocusEvent e) {
                if (((JTextComponente.getComponent()).getText().isEmpty()) {
                    ((JTextComponente.getComponent()).setText(defaultMessage);
                    deleteOnClick = true;
                }
            }

            @Override
            public void focusGained(final FocusEvent e) {
                //clear when user types
                if (deleteOnClick) {
                    deleteOnClick = false;
                    ((JTextComponente.getComponent()).setText("");
                }
            }
        });

    }

    private void addComponentsToInterface() {
        if (style == SEARCH_EXTERNAL_WINDOW || style == SEARCH_TABBED_PANE) {

            //build frame
            final JScrollPane scrollPane = new JScrollPane();
            scrollPane.getViewport().add(resultsList);
            scrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED);
            scrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED);
            scrollPane.getVerticalScrollBar().setUnitIncrement(80);
            scrollPane.getHorizontalScrollBar().setUnitIncrement(80);

            getContentPane().setLayout(new BorderLayout());
            getContentPane().add(scrollPane, BorderLayout.CENTER);
            getContentPane().add(nav, BorderLayout.NORTH);
            getContentPane().add(advancedPanel, BorderLayout.SOUTH);

            //position and size
            final Container frame = currentGUI.getFrame();

            if (style == SEARCH_EXTERNAL_WINDOW) {
                final int w = 230;

                final int h = frame.getHeight();
                final int x1 = frame.getLocationOnScreen().x;
                int x = frame.getWidth() + x1;
                final int y = frame.getLocationOnScreen().y;
                final Dimension d = Toolkit.getDefaultToolkit().getScreenSize();

                final int width = d.width;
                if (x + w > width) {
                    x = width - w;
                    frame.setSize(x - x1, frame.getHeight());
                }

                setSize(w, h);
                setLocation(x, y);
            }
            searchAll.setFocusable(false);

            searchText.grabFocus();

            if (style == SEARCH_TABBED_PANE) {
                currentGUI.setSearchText(searchText);
            }
        else {
            //Whole Panel not used, take what is needed
            currentGUI.setSearchText(searchText);
        }
    }

    /**
     * Find text in PDF without using the search window. This will not initialise the
     * user interface when performing searches.
     *
     @param dec            PdfDecoderInt object used for the PDF to be searched
     @param values         Values object used by the user interface
     @param searchType     int value using bit flags to control search options
     @param listOfTerms    true to treat search value as a space separated list, false to treat as a single search term
     @param singlePageOnly true to search just the current page, false to search the entire document
     @param searchValue    String value to be searched, this can be either a single search term or a space separated list of terms controlled by listOfTerms
     */
    public void findWithoutWindow(final PdfDecoderInt dec, final Values values, final int searchType, final boolean listOfTerms, final boolean singlePageOnly, final String searchValue) {

        init(dec, values);

        if (!commonValues.isSearching()) {
            backGroundSearch = true;
            commonValues.setIsSearching(true);

            decode_pdf = dec;
            commonValues = values;

            if (!listOfTerms) { // find exact word or phrase
                searchTerms = new String[]{searchValue};
            else // match any of the words
                searchTerms = searchValue.split(" ");
                for (int i = 0; i < searchTerms.length; i++) {
                    searchTerms[i= searchTerms[i].trim();
                }
            }

            searchTypeParameters = searchType;

            singlePageSearch = singlePageOnly;

            find(dec, values);

        else {
            currentGUI.showMessageDialog("Please wait for search to finish before starting another.");
        }
    }

    /**
     * Find text in PDF using the search window. If the search window has not been
     * initialised it will be initialised here.
     *
     @param dec    PdfDecoderInt object used for the PDF to be searched
     @param values Values object used by the user interface
     */
    public void find(final PdfDecoderInt dec, final Values values) {

        //pop up new window to search text (initialise if required
        if (!backGroundSearch) {
            init(dec, values);
            if (style == SEARCH_EXTERNAL_WINDOW) {
                setVisible(true);
            }
        else {
            try {
                searchText();
            catch (final Exception e) {
                LogWriter.writeLog(e);
            }
        }

    }

    /**
     * Remove the search window from view and clear the results if required.
     *
     @param justHide true to keep the results, false to clear results
     */
    public void removeSearchWindow(final boolean justHide) {

        setVisible(false);

        if (isSetup && !justHide) {
            if (listModel != null) {
                listModel.clear();
            }

            itemFoundCount = 0;
            commonValues.setIsSearching(false);

        }

        //lose any highlights and force redraw with non-existent box
        if (decode_pdf != null) {
            decode_pdf.getTextLines().clearHighlights();
            decode_pdf.repaint();
        }
    }

    /**
     * Clears the current set of results from the result list
     */
    private void clearCurrentResults() {
        listModel.clear();
        resultsList.clearSelection();
        textPages.clear();
        textRectangles.clear();

        itemFoundCount = 0;
        decode_pdf.getTextLines().clearHighlights();

        //Refresh display after clearing highlights
        decode_pdf.repaintPane(0);
        decode_pdf.getPages().refreshDisplay();
        resultsList.repaint();
    }

    private void searchText() {

        //Flag is we are using menu bar search
        usingMenuBarSearch = style == SEARCH_MENU_BAR;

        //Alter searchKey so the update thread knows not to update
        searchKey++;

        //Reset last page searched flag.
        lastPage = -1;

        /*
         * To prevent the chance of hitting the maximum value of searchKey
         * we should reset long after a value large enough to guarantee
         * any thread using a searchKey of 0 is closed.
         */
        if (searchKey > 3_000) {
            searchKey = 0;
        }

        //Cancel a search if currently exists
        if (searchFuture != null) {
            searchFuture.cancel(true);
        }

        if (!usingMenuBarSearch && (searchTypeParameters & SearchType.SEARCH_HIGHLIGHTS_ONLY== SearchType.SEARCH_HIGHLIGHTS_ONLY) {
            searchAreas = decode_pdf.getTextLines().getAllHighlights();
        else {
            searchAreas = null;
        }

        searchFuture = service.submit(searchRunner);
    }

    /**
     * Performs the currently set up search on the given page
     *
     @param page       :: Page to be searched with the currently set term and settings
     @param currentKey :: The current search key, used to control results update when search ends
     @param model      :: [AWI] The list model to append the search results to
     */
    private void searchPage(final int page, final int currentKey, final DefaultListModel<String> modelthrows Exception {
        final PdfPageData currentPageData = decode_pdf.getPdfPageData();
        final int x1 = currentPageData.getMediaBoxX(page);
        final int x2 = currentPageData.getMediaBoxWidth(page+ x1;
        final int y2 = currentPageData.getMediaBoxY(page);
        final int y1 = currentPageData.getMediaBoxHeight(page+ y2;
        searchPage(page, x1, y1, x2, y2, currentKey, model);
    }

    /**
     * Performs the currently set up search on the given page
     *
     @param x1         the left x cord
     @param y1         the upper y cord
     @param x2         the right x cord
     @param y2         the lower y cord
     @param page       :: Page to be searched with the currently set term and settings
     @param currentKey :: The current search key, used to control results update when search ends
     @param model      :: [AWI] The list model to append the search results to
     */
    private void searchPage(final int page, final int x1, final int y1, final int x2, final int y2, final int currentKey, final DefaultListModel<String> modelthrows Exception {

        firstPageWithResults = 0;
        final PdfGroupingAlgorithms grouping;

        final PdfPageData pageSize = decode_pdf.getPdfPageData();

        if (decode_pdf.getDisplayView() == Display.SINGLE_PAGE && page == commonValues.getCurrentPage()) {
            grouping = decode_pdf.getGroupingObject();
        else {
            decode_pdf.decodePageInBackground(page);
            grouping = decode_pdf.getBackgroundGroupingObject();
        }

        final DefaultSearchListener listener = new DefaultSearchListener();

        // tell JPedal we want teasers
        grouping.generateTeasers();

        //allow us to add options
        grouping.setIncludeHTML(true);

        //Set search term in results list
        final StringBuilder term = new StringBuilder();
        for (int i = 0; i != searchTerms.length; i++) {
            term.append(searchTerms[i]).append(' ');
        }
        resultsList.setSearchTerm(term.toString());

        final SortedMap<Object, String> highlightsWithTeasers =
                grouping.findTextWithinInAreaWithTeasers(x1, y1, x2, y2, pageSize.getRotation(page), searchTerms, searchTypeParameters, listener);

        //update data structures with results from this page
        if (!highlightsWithTeasers.isEmpty()) {

            itemFoundCount += highlightsWithTeasers.size();

            for (final Map.Entry<Object, String> e : highlightsWithTeasers.entrySet()) {
                /*highlight is a rectangle or a rectangle[]*/
                final Object highlight = e.getKey();

                final String teaser = e.getValue();

                // [AWI]: Only push the results off to the EDT if the model is the
                // current active list model.
                if (!SwingUtilities.isEventDispatchThread() && model == listModel) {
                    final Runnable setTextRun = () -> {
                        if (currentKey == searchKey) {
                            //if highights ensure displayed by wrapping in tags
                            if (!teaser.contains("<b>")) {
                                model.addElement(teaser);
                            else {
                                model.addElement("<html>" + teaser + "</html>");
                            }
                        }
                    };
                    SwingUtilities.invokeLater(setTextRun);

                else {
                    if (!teaser.contains("<b>")) {
                        model.addElement(teaser);
                    else {
                        model.addElement("<html>" + teaser + "</html>");
                    }
                }

                final Integer key = textRectangles.size();
                textRectangles.put(key, highlight);
                textPages.put(key, page);

                if (page < firstPageWithResults || firstPageWithResults == 0) {
                    firstPageWithResults = page;
                }
            }

        }

        lastPage = page;

    }

    /**
     * Get the first page with results
     *
     @return int value for the page of the first result
     */
    public int getFirstPageWithResults() {
        return firstPageWithResults;
    }

    /**
     * Have search text input grab focus
     */
    public void grabFocusInInput() {
        searchText.grabFocus();

    }

    /**
     * Gets the visibility of the search interface (external window style only)
     *
     @return true is visible, otherwise false
     */
    public boolean isSearchVisible() {
        return isVisible();
    }

    /**
     * Set the view style for the search interface using a flag to control the style
     <p>
     * For instance the search interface can be displayed as follows.
     * setViewStyle(SwingSearchWindow.SEARCH_EXTERNAL_WINDOW); // Show search interface as external window
     * setViewStyle(SwingSearchWindow.SEARCH_TABBED_PANE); // Show search interface on side tab bar
     * setViewStyle(SwingSearchWindow.SEARCH_MENU_BAR); // Show search interface on button tool bar
     *
     @param style int value representing the search style
     */
    public void setViewStyle(final int style) {
        this.style = style;
    }

    /**
     * Get the view style being used by the search interface
     *
     @return int value representing the search style
     */
    public int getViewStyle() {
        return style;
    }

    /**
     * Set the value of the search input
     *
     @param s String value to be used as search input
     */
    public void setSearchText(final String s) {
        deleteOnClick = false;
        searchText.setText(s);
    }

    /**
     * Get the search result areas for the current results
     *
     @return Map object containing result areas using result index as keys
     */
    public Map<Integer, Object> getTextRectangles() {
        return Collections.unmodifiableMap(textRectangles);
    }

    /**
     * Get the search result list
     *
     @return SearchList containing all search result data
     */
    public SearchList getResults() {
        return resultsList;
    }

    /**
     * Perform a search on a single page and return the results
     *
     @param page int value for the page to be searched
     @return SearchList containing the results of the search
     */
    public SearchList getResults(final int page) {

        usingMenuBarSearch = style == SEARCH_MENU_BAR;

        if (page != lastPage && usingMenuBarSearch) {

            try {

                searchKey++;
                if (searchKey > 3_000) {
                    searchKey = 0;
                }

                clearCurrentResults();

                // [AWI]: Updated for changes to searchPage method
                searchPage(page, searchKey, listModel);

            catch (final Exception e) {
                LogWriter.writeLog(e);
            }

        }

        return resultsList;
    }

    /**
     * Reset search text and menu bar buttons when opening new page
     */
    public void resetSearchWindow() {
        if (isSetup) {

            searchText.setText(defaultMessage);
            deleteOnClick = true;

            // [AWI] Reset the result status text label (itemFoundCount should be reset already
            // by the removeSearchWindow method)
            searchCount.setText(Messages.getMessage("PdfViewerSearch.ItemsFound"' ' + itemFoundCount);

            if (hasSearched) {
                currentGUI.getButtons().getButton(Commands.NEXTRESULT).setEnabled(false);
                currentGUI.getButtons().getButton(Commands.PREVIOUSRESULT).setEnabled(false);
                hasSearched = false;
            }
            decode_pdf.requestFocus();
        }
    }

    /**
     * Dispose of the search window
     */
    @Override
    public void dispose() {

        if (searchFuture != null) {
            searchFuture.cancel(true);
            clearCurrentResults();
        }

        if (resultsList != null) {
            for (final ListSelectionListener listener : resultsList.getListSelectionListeners()) {
                resultsList.removeListSelectionListener(listener);
            }
        }
        resultsList = null;

        if (searchButton != null) {
            for (final ActionListener listener : searchButton.getActionListeners()) {
                searchButton.removeActionListener(listener);
            }
        }
        searchButton = null;

        if (searchText != null) {
            for (final KeyListener listener : searchText.getKeyListeners()) {
                searchText.removeKeyListener(listener);
            }

            for (final FocusListener listener : searchText.getFocusListeners()) {
                searchText.removeFocusListener(listener);
            }
        }
        searchText = null;

        if (label != null) {
            for (final MouseListener listener : label.getMouseListeners()) {
                label.removeMouseListener(listener);
            }
        }
        label = null;

        for (final WindowListener listener : getWindowListeners()) {
            removeWindowListener(listener);
        }

        for (final PropertyChangeListener listener : getPropertyChangeListeners()) {
            removePropertyChangeListener(listener);
        }
        for (final ComponentListener listener : getComponentListeners()) {
            removeComponentListener(listener);
        }
        for (final MouseListener listener : getMouseListeners()) {
            removeMouseListener(listener);
        }
        for (final FocusListener listener : getFocusListeners()) {
            removeFocusListener(listener);
        }
        if (searchAreas != null) {
            searchAreas.clear();
        }
        searchAreas = null;

        if (listModel != null) {
            listModel.clear();
        }
        listModel = null;

        if (advancedPanel != null) {

            for (final PropertyChangeListener listener : advancedPanel.getPropertyChangeListeners()) {
                advancedPanel.removePropertyChangeListener(listener);
            }
            advancedPanel.removeAll();
        }
        advancedPanel = null;
        removeAll();
        decode_pdf = null;
        service.shutdown();
        super.dispose();
    }

    private int getSearchOptionsAsInt() {
        int searchOptions = SearchType.DEFAULT;

        if (wholeWordsOnlyBox.isSelected()) {
            searchOptions |= SearchType.WHOLE_WORDS_ONLY;
        }

        if (caseSensitiveBox.isSelected()) {
            searchOptions |= SearchType.CASE_SENSITIVE;
        }

        if (multiLineBox.isSelected()) {
            searchOptions |= SearchType.MUTLI_LINE_RESULTS;
        }

        if (highlightAll.isSelected()) {
            searchOptions |= SearchType.HIGHLIGHT_ALL_RESULTS;
        }

        if (useRegEx.isSelected()) {
            searchOptions |= SearchType.USE_REGULAR_EXPRESSIONS;
        }

        if (ignoreWhiteSpace.isSelected()) {
            searchOptions |= SearchType.IGNORE_SPACE_CHARACTERS;
        }

        if (searchHighlightedOnly.isSelected()) {
            searchOptions |= SearchType.SEARCH_HIGHLIGHTS_ONLY;
        }

        return searchOptions;
    }

    /**
     * [AWI] Focus listener used to detect when the search text field has been given focus.</p>
     <p>
     * This class was added to support detecting when Keyboard input is needed for systems using a touchscreen monitor
     * with a virtual keyboard.
     */
    class SearchTextFocusListener extends FocusAdapter {
        @Override
        public void focusGained(final FocusEvent e) {
            if (decode_pdf != null) {
                final Object handler = decode_pdf.getExternalHandler(Options.KeyboardReadyHandler);
                if (handler instanceof JPedalActionHandler) {
                    ((JPedalActionHandlerhandler).actionPerformed(currentGUI, null);
                }
            }
        }
    }

    class searchListListener implements ListSelectionListener {

        @Override
        public void valueChanged(final ListSelectionEvent e) {
            /*
             * Only do something on mouse button up,
             * prevents this code being called twice
             * on mouse click
             */
            if (!e.getValueIsAdjusting()) {

                if (!Values.isProcessing()) {

                    final int id = resultsList.getSelectedIndex();

                    decode_pdf.getTextLines().clearHighlights();

                    if (id != -1) {

                        final Integer newPage = textPages.get(id);

                        if (newPage != null) {

                            //move to new page
                            if (commonValues.getCurrentPage() != newPage) {
                                PageNavigator.gotoPage(newPage.toString(), currentGUI, commonValues, decode_pdf);
                            }

                            while (Values.isProcessing()) {
                                //Ensure page has been processed else highlight may be incorrect
                                try {
                                    Thread.sleep(500);
                                catch (final InterruptedException e2) {
                                    LogWriter.writeLog(e2);
                                }
                            }

                            //Highlight all search results on page.
                            if ((searchTypeParameters & SearchType.HIGHLIGHT_ALL_RESULTS== SearchType.HIGHLIGHT_ALL_RESULTS) {
                                highlightAllSearchResults();

                            else {
                               highlightSingleSearchResults(id);
                            }

                            //Refresh display after clearing highlights
                            decode_pdf.repaintPane(0);
                            decode_pdf.getPages().refreshDisplay();
                        }
                    }
                }

                //When page changes make sure only relevant navigation buttons are displayed
                currentGUI.getButtons().setBackNavigationButtonsEnabled(commonValues.getCurrentPage() != 1);

                currentGUI.getButtons().setForwardNavigationButtonsEnabled(commonValues.getCurrentPage() != decode_pdf.getPageCount());

            else {
                resultsList.repaint();

            }
        }

     private void highlightAllSearchResults() {
         int[][] showAllOnPage;
         Vector_Rectangle_Int storageVector = new Vector_Rectangle_Int();
         int lastPage = -1;
         for (int k = 0; k != resultsList.getModel().getSize(); k++) {
             final Integer page = textPages.get(k);

             if (page != null) {

                 final int currentPage = page;
                 if (currentPage != lastPage) {
                     storageVector.trim();
                     showAllOnPage = storageVector.get();

                     decode_pdf.getTextLines().addHighlights(showAllOnPage, true, lastPage);
                     lastPage = currentPage;
                     storageVector = new Vector_Rectangle_Int();
                 }

                 final Object highlight = textRectangles.get(k);

                 if (highlight instanceof int[]) {
                     storageVector.addElement((int[]) highlight);
                 }
                 if (highlight instanceof int[][]) {
                     final int[][] areas = (int[][]) highlight;
                     for (int i = 0; i != areas.length; i++) {
                         storageVector.addElement(areas[i]);
                     }
                 }
             }
         }
         storageVector.trim();
         showAllOnPage = storageVector.get();

         decode_pdf.getTextLines().addHighlights(showAllOnPage, true, lastPage);
     }

     private void highlightSingleSearchResults(final int id) {
         final int currentPage = textPages.get(id);

         final Vector_Rectangle_Int storageVector = new Vector_Rectangle_Int();
         int[] scroll = null;
         final Object highlight = textRectangles.get(id);
         if (highlight instanceof int[]) {
             storageVector.addElement((int[]) highlight);
             scroll = (int[]) highlight;
         }

         if (highlight instanceof int[][]) {
             final int[][] areas = (int[][]) highlight;
             scroll = areas[0];
             for (int i = 0; i != areas.length; i++) {
                 storageVector.addElement(areas[i]);
             }
         }

         if (scroll != null) {
             Scroll.rectToHighlight(scroll, currentPage, decode_pdf);
         }
         storageVector.trim();
         decode_pdf.getTextLines().addHighlights(storageVector.get(), true, currentPage);
     }
    }

}