/**************************************************************************
 OmegaT - Computer Assisted Translation (CAT) tool 
          with fuzzy matching, translation memory, keyword search, 
          glossaries, and translation leveraging into updated projects.

 Copyright (C) 2000-2006 Keith Godfrey and Maxym Mykhalchuk
               2006 Henry Pijffers
               2009 Didier Briel
               2010 Martin Fleurke, Antonio Vilei, Didier Briel
               2012 Didier Briel, Thomas Cordonnier
               2014-2019 Thomas Cordonnier
               Home page: http://www.omegat.org/
               Support center: http://groups.yahoo.com/group/OmegaT/

 This program is free software; you can redistribute it and/or modify
 it under the terms of the GNU General Public License as published by
 the Free Software Foundation; either version 2 of the License, or
 (at your option) any later version.

 This program is distributed in the hope that it will be useful,
 but WITHOUT ANY WARRANTY; without even the implied warranty of
 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 GNU General Public License for more details.

 You should have received a copy of the GNU General Public License
 along with this program; if not, write to the Free Software
 Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
 **************************************************************************/

package org.omegat.gui.search;

import java.awt.Color;
import java.awt.Container;
import java.awt.GridLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.awt.event.WindowEvent;
import java.awt.event.MouseEvent;
import java.awt.event.MouseAdapter;
import java.awt.BorderLayout;

import java.io.PrintWriter;
import java.io.BufferedWriter;
import java.io.FileWriter;

import java.util.Collection;
import java.util.Map;
import java.util.Set;

import javax.swing.AbstractButton;
import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.Box;
import javax.swing.BorderFactory;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JComponent;
import javax.swing.JRadioButton;
import javax.swing.JScrollPane;
import javax.swing.JSpinner;
import javax.swing.JTextField;
import javax.swing.JComboBox;
import javax.swing.DefaultComboBoxModel; 
import javax.swing.text.DefaultStyledDocument;

import javax.swing.KeyStroke;
import javax.swing.SpinnerModel;
import javax.swing.SpinnerNumberModel;
import javax.swing.event.ChangeEvent;

import javax.swing.event.ChangeListener;
import javax.swing.text.Document;
import javax.swing.text.PlainDocument;
import javax.swing.text.StringContent;
import javax.swing.undo.UndoManager;
import javax.swing.JComboBox;
import javax.swing.DefaultComboBoxModel;
import javax.swing.JProgressBar;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.text.DefaultStyledDocument;
import javax.swing.text.JTextComponent;
 
import org.omegat.core.Core;
import org.omegat.core.data.ProjectOptions;
import org.omegat.core.events.IProjectEventListener;
import org.omegat.core.search.SearchResultEntry;
import org.omegat.core.search.Searcher;
import org.omegat.gui.main.MainWindow;
import org.omegat.gui.editor.IPopupMenuConstructor;
import org.omegat.tokenizer.ITokenizer;
import org.omegat.util.Log;
import org.omegat.util.OConsts;
import org.omegat.util.OStrings;
import org.omegat.util.Preferences;
import org.omegat.util.StringUtil;
import org.omegat.util.gui.Styles;
import org.omegat.util.gui.UIThreadsUtil;
import org.openide.awt.Mnemonics;

/**
 * This is a window that appears when user'd like to search for something. For
 * each new user's request new window is created. Actual search is done by
 * Searcher Thread.
 * 
 * @author Keith Godfrey
 * @author Henry Pijffers (henry.pijffers@saxnot.com)
 * @author Didier Briel
 * @author Martin Fleurke
 * @author Antonio Vilei
 * @author Thomas Cordonnier
 */
@SuppressWarnings("serial")
public abstract class SearchWindow extends JFrame {
    public SearchWindow(MainWindow par, String startText) {
        // super(par, false);
        setTitle(OStrings.getString("SW_TITLE_SEARCH"));

        // Handle escape key to close the window
        KeyStroke escape = KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0, false);
        Action escapeAction = new AbstractAction() {
            public void actionPerformed(ActionEvent e) {
                doCancel();
            }
        };
        getRootPane().getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(escape, "ESCAPE");
        getRootPane().getActionMap().put("ESCAPE", escapeAction);
        
        Container cp = getContentPane();
        cp.setLayout(new BorderLayout ());
		
        m_viewer = new EntryListPane(this);
        cp.add(new JScrollPane(m_viewer), BorderLayout.CENTER);		
		
        Box bTop = Box.createVerticalBox();
        cp.add(bTop, BorderLayout.NORTH);
        
        bTop.add (textPanel(startText));
        bTop.add (modePanel());
        JComponent options = optionsPanel(); if (options != null) bTop.add (options);
        JComponent where = wherePanel(); if (where != null) bTop.add (where);

        m_resultsLabel = new JLabel();

        advancedPanel = advancedPanel();
        if (advancedPanel != null) bTop.add (advancedPanel); // must come before		
        
        Box bAO = Box.createHorizontalBox();
        bAO.add(m_resultsLabel);
        bAO.add(Box.createHorizontalGlue());
        // bAO.add(m_progressBar = new JProgressBar()); // m_progressBar.setVisible(false);
        
        m_searchButton = new JButton();
        m_searchButton.addActionListener(e -> { doSearch(false); });	
        Mnemonics.setLocalizedText(m_searchButton, OStrings.getString("BUTTON_SEARCH"));
		m_stopSearchButton = new JButton();
        m_stopSearchButton.addActionListener(e -> { stopAndDisplay(); });	
        Mnemonics.setLocalizedText(m_stopSearchButton, OStrings.getString("BUTTON_SEARCH_STOP"));
		m_stopSearchButton.setEnabled(false); // will be enabled after search starts
        bAO.add(m_stopSearchButton); bAO.add(m_searchButton);
                
        if (advancedPanel != null) {
            m_advancedButton = new JButton();
            Mnemonics.setLocalizedText(m_advancedButton, OStrings.getString("SW_SHOW_ADVANCED_OPTIONS"));
            
            // Box for advanced options button
            bAO.add(m_advancedButton);
            bAO.add(Box.createHorizontalStrut(H_MARGIN));
            
            m_advancedButton.addActionListener(e -> {
                m_advancedVisible = !m_advancedVisible;
                updateAdvancedOptionStatus();
            });
            
            bTop.add (bAO);			
        } 
        
        cp.add (buttonsPanel(), BorderLayout.SOUTH);

        m_viewer.setText(getHelpText());
        
        loadPreferences();
		
		for (JTextComponent comp: textFieldsList())
			comp.getDocument().addDocumentListener(new DocumentListener() {
				// Invalidate replacement if search or replace strings change.
				// Otherwise you can accidentally do the wrong thing like:
				// 1. Search for "foo"
				// 2. Enter "bar" in replacement field
				// 3. Hit "Replace all"
				// => You replaced "foo" with "" because you didn't re-search after
				// entering "bar"
				@Override
				public void removeUpdate(DocumentEvent e) {
					for (JComponent comp: componentsEnabledWhenResults()) comp.setEnabled(false);
				}

				@Override
				public void insertUpdate(DocumentEvent e) {
					for (JComponent comp: componentsEnabledWhenResults()) comp.setEnabled(false);
				}

				@Override
				public void changedUpdate(DocumentEvent e) {
					for (JComponent comp: componentsEnabledWhenResults()) comp.setEnabled(false);
				}
			});
    }
    
    protected abstract String getScopeText();
    protected abstract boolean isReplace();
    
    public final String getHelpText() {
        StringBuffer buf = new StringBuffer();
        buf.append(OStrings.getString("SW_SEARCH_SCOPE")).append(":\n");
        buf.append(getScopeText());
        buf.append("\n\n").append(OStrings.getString("SW_ZONE_EXPRESSION_MODE")).append(":\n");
        buf.append(OStrings.getString("SW_HELP_EXPR_MODE_EXACT")).append("\n");
        if (!isReplace()) buf.append(OStrings.getString("SW_HELP_EXPR_MODE_KEYWORDS")).append("\n");
        buf.append(OStrings.getString("SW_HELP_EXPR_MODE_REGEX")).append("\n");
        buf.append(OStrings.getString("SW_HELP_EXPR_MODE_REGEX_NOWILD")).append("\n");
        if (isReplace()) buf.append(OStrings.getString("SW_HELP_EXPR_MODE_REGEX_REPLACE")).append("\n");
        buf.append("\n\n").append(OStrings.getString("SW_ZONE_WORD_MODE")).append(":\n");
        buf.append(OStrings.getString("SW_WORD_MODE_STRINGS")).append(":\t").append(OStrings.getString("SW_HELP_WORD_MODE_STRINGS")).append("\n");
        buf.append(OStrings.getString("SW_WORD_MODE_WHOLE")).append(":\t").append(OStrings.getString("SW_HELP_WORD_MODE_WHOLE")).append("\n");
        buf.append(OStrings.getString("SW_WORD_MODE_LEMMAS")).append(":\t").append(OStrings.getString("SW_HELP_WORD_MODE_LEMMAS")).append("\n");        
        return buf.toString();
    }
    
    @Override
    public void setVisible (boolean visible) {
        super.setVisible (visible);
        getMainSearchTextField().requestFocus();
        
    }

    protected abstract JComponent textPanel(String startText);	
    
     protected JComponent modePanel() {
        m_modePanel = new SearchModeBox(javax.swing.BoxLayout.X_AXIS, ITokenizer.StemmingMode.MATCHING, this);
        
        for (AbstractButton component: m_modePanel.getOptionsComponents())
            addFocusToSearchListener(component);
            
        return m_modePanel;
    }
    
    protected Box optionsPanel() {
        // <Box 3 - Search options>
        m_removeDupCB = new JCheckBox();
        Mnemonics.setLocalizedText(m_removeDupCB, OStrings.getString("SW_REMOVE_DUP"));
        m_trueMissingCB = new JCheckBox();
        Mnemonics.setLocalizedText(m_trueMissingCB, OStrings.getString("SW_TRUE_MISSING"));

        // box OptionsBox bOB
        Box bOB = Box.createHorizontalBox();
        bOB.add(m_removeDupCB); bOB.add(m_trueMissingCB);

        addFocusToSearchListener(m_removeDupCB); addFocusToSearchListener(m_trueMissingCB);

        bOB.setBorder (BorderFactory.createTitledBorder(BorderFactory.createLineBorder(Color.black), OStrings.getString("SW_ZONE_OPTIONS")));

        return bOB;
    }
    
    protected abstract JComponent wherePanel();
    
    protected abstract String[] getFormatVariablesList();
    protected abstract String getFormatOptionName();
    protected abstract String getFormatOptionDefaultValue();
    
    public abstract void refreshLists(boolean removeContents);
    
    protected JComponent advancedPanel() {
        Box advBox = Box.createVerticalBox();
    
        // Box Number of results
        SpinnerNumberModel m_numberModel = new SpinnerNumberModel(OConsts.ST_MAX_SEARCH_RESULTS, 1, Integer.MAX_VALUE, 1);
        m_numberOfResults = new JSpinner(m_numberModel);
        JLabel m_numberLabel = new JLabel();
        Mnemonics.setLocalizedText(m_numberLabel, OStrings.getString("SW_NUMBER"));
        m_maxTime = new JSpinner(new SpinnerNumberModel(10, 1, Integer.MAX_VALUE, 1));
        JLabel m_timeLabel = new JLabel();
        Mnemonics.setLocalizedText(m_timeLabel, OStrings.getString("SW_MAX_TIME"));
        Box bNbr = Box.createHorizontalBox();
        bNbr.add(m_numberLabel);
        bNbr.add(m_numberOfResults);
        bNbr.add(m_timeLabel);
        bNbr.add(m_maxTime);
        bNbr.add(Box.createHorizontalStrut(H_MARGIN));
        bNbr.add(Box.createHorizontalGlue());
		bNbr.add(m_elapsedTimeLabel = new JLabel());
        // Dummy field to limit previous field size
        JSpinner m_dummy = new JSpinner();
        m_dummy.setVisible(false);
        bNbr.add(m_dummy);
        bNbr.add(Box.createHorizontalStrut(H_MARGIN));
        bNbr.add(Box.createHorizontalGlue());
        bNbr.add(m_dummy);

        Box formatBox = Box.createHorizontalBox();
        formatBox.setBorder (BorderFactory.createTitledBorder(BorderFactory.createLineBorder(Color.black), OStrings.getString("EXT_TMX_MATCHES_TEMPLATE")));
        advBox.add (formatBox);
        final JLabel varLabel = new JLabel();
        Mnemonics.setLocalizedText(varLabel, OStrings.getString("EXT_TMX_MATCHES_TEMPLATE_VARIABLES"));
        formatBox.add(varLabel);
        final JComboBox varList = new JComboBox();
        formatBox.add(varList);
        varList.setModel(new DefaultComboBoxModel(getFormatVariablesList()));
        varList.setEnabled (false);
        final JButton insertButton = new JButton();
        Mnemonics.setLocalizedText(insertButton, OStrings.getString("BUTTON_INSERT"));
        insertButton.addActionListener (e -> { m_viewer.replaceSelection(varList.getSelectedItem().toString()); });
        insertButton.setEnabled (false);
        formatBox.add(insertButton);
        final JButton resetButton = new JButton();
        Mnemonics.setLocalizedText(resetButton, "Reset");
        resetButton.addActionListener (e -> { m_viewer.setText (Preferences.getPreferenceDefault(getFormatOptionName(), getFormatOptionDefaultValue())); });
        resetButton.setEnabled (false);
        formatBox.add(resetButton);		
        final JButton cancelButton = new JButton();
        cancelButton.setEnabled (false);
        formatBox.add(cancelButton);
        Mnemonics.setLocalizedText(cancelButton, OStrings.getString("BUTTON_CANCEL"));
        m_formatButton = new JButton();
        Mnemonics.setLocalizedText(m_formatButton, OStrings.getString("SW_BTN_CONFIGURE"));
        formatBox.add(m_formatButton);
        final class ConfigureActionListener implements ActionListener {
			private final org.omegat.util.gui.RegexHighlightListener REGEX_LISTENER = new org.omegat.util.gui.RegexHighlightListener(m_viewer, org.omegat.util.gui.RegexHighlightListener.MODE_TEMPLATE_ML);
			
            private String previousText;
            private final ActionListener resetListener = new ActionListener() {
                public void actionPerformed(ActionEvent e) {
                    m_viewer.setText(previousText);
                    m_viewer.setBackground (java.awt.Color.WHITE);
					m_viewer.getDocument().removeDocumentListener(REGEX_LISTENER);
                    m_viewer.setEditable (false);

                    Mnemonics.setLocalizedText(m_formatButton, OStrings.getString("SW_BTN_CONFIGURE"));
                    m_formatButton.removeActionListener (validateListener);
                    cancelButton.removeActionListener (this);
                    m_formatButton.addActionListener (ConfigureActionListener.this);
                    
                    varList.setEnabled (false);
                    insertButton.setEnabled (false);
                    resetButton.setEnabled(false);
                    cancelButton.setEnabled (false);
                    m_searchButton.setEnabled (isSearchPossible());
                }
            };
            private final ActionListener validateListener = new ActionListener() {
                public void actionPerformed(ActionEvent e) {					
                    Preferences.setPreference(getFormatOptionName(), m_viewer.getText());
                    Preferences.save();
                    
                    resetListener.actionPerformed (e);
                }
            };
            
            public void actionPerformed(ActionEvent e) {
                if (m_thread != null) m_thread.fin();
                m_searchButton.setEnabled (false);
            
                previousText = m_viewer.getText();
                m_viewer.setText (Preferences.getPreferenceDefault(getFormatOptionName(), getFormatOptionDefaultValue()));
                m_viewer.setBackground (new java.awt.Color (0xFF, 0xE0, 0xFF));
				m_viewer.getDocument().addDocumentListener(REGEX_LISTENER);
                m_viewer.setEditable (true);

                Mnemonics.setLocalizedText(m_formatButton, OStrings.getString("SW_BTN_CONFIGURE_VALIDATE"));
                m_formatButton.removeActionListener (this);
                m_formatButton.addActionListener (validateListener);
                
                varList.setEnabled (true);
                insertButton.setEnabled (true);
                cancelButton.setEnabled (true);
                resetButton.setEnabled(true);
                cancelButton.addActionListener (resetListener);
            }
        };
        m_formatButton.addActionListener (new ConfigureActionListener());
        
        m_numberOfResults.addChangeListener((ChangeEvent e) -> {
            // move focus to search edit field
            getMainSearchTextField().requestFocus();
        });
        
        advBox.add (bNbr);
        return advBox;
    }
    
    protected Box buttonsPanel() {
        // box CheckBox
        JButton m_dismissButton = new JButton();
        Mnemonics.setLocalizedText(m_dismissButton, OStrings.getString("BUTTON_CLOSE"));
        Box bCB = Box.createHorizontalBox();
        bCB.add(m_dismissButton);

        // ///////////////////////////////////
        // action listeners
        m_dismissButton.addActionListener(getRootPane().getActionMap().get("ESCAPE"));
        
        return bCB;
    }
    
    public abstract JComboBox getMainSearchTextField();

    protected boolean isSearchPossible() {
        return true;
    }
    
    protected final void addFocusToSearchListener(AbstractButton button) {
        button.addActionListener (e -> {
                // move focus to search edit field
                getMainSearchTextField().requestFocus();
                m_searchButton.setEnabled (isSearchPossible());
            });
    }
        
    /**
     * Loads the position and size of the search window and the button selection
     * state.
     */
    protected void loadPreferences() {
        // window size and position
        try {
            String dx = Preferences.getPreferenceDefault(this.getClass().getSimpleName() + "_" + Preferences.SEARCHWINDOW_X, Preferences.getPreference(Preferences.SEARCHWINDOW_X));
            String dy = Preferences.getPreferenceDefault(this.getClass().getSimpleName() + "_" + Preferences.SEARCHWINDOW_Y, Preferences.getPreference(Preferences.SEARCHWINDOW_Y));
            setLocation(Integer.parseInt(dx), Integer.parseInt(dy));
            String dw = Preferences.getPreferenceDefault(this.getClass().getSimpleName() + "_" + Preferences.SEARCHWINDOW_WIDTH, Preferences.getPreference(Preferences.SEARCHWINDOW_WIDTH));
            String dh = Preferences.getPreferenceDefault(this.getClass().getSimpleName() + "_" + Preferences.SEARCHWINDOW_HEIGHT, Preferences.getPreference(Preferences.SEARCHWINDOW_HEIGHT));
            setSize(Integer.parseInt(dw), Integer.parseInt(dh));
        } catch (NumberFormatException nfe) {
            // set default size and position
            setSize(800, 700);
        }

        m_modePanel.loadPreferences(this.getClass().getSimpleName() + "_");

        // all results
        if (m_removeDupCB != null) m_removeDupCB.setSelected(Preferences.isPreferenceDefault(this.getClass().getSimpleName() + "_" + Preferences.SEARCHWINDOW_REMOVE_DUP, Preferences.isPreferenceDefault(Preferences.SEARCHWINDOW_REMOVE_DUP, false)));
        if (m_trueMissingCB != null) m_trueMissingCB.setSelected(Preferences.isPreferenceDefault(this.getClass().getSimpleName() + "_" + Preferences.SEARCHWINDOW_TRUE_MISSING, Preferences.isPreferenceDefault(Preferences.SEARCHWINDOW_TRUE_MISSING, false)));

        // load advanced options settings from user preferences
        loadAdvancedOptionPreferences();
        // update advanced options status
        updateAdvancedOptionStatus();
    }

    /**
     * Saves the size and position of the search window and the button selection
     * state
     */
    protected void savePreferences() {
        // window size and position
        Preferences.setPreference(this.getClass().getSimpleName() + "_" + Preferences.SEARCHWINDOW_WIDTH, getWidth());
        Preferences.setPreference(this.getClass().getSimpleName() + "_" + Preferences.SEARCHWINDOW_HEIGHT, getHeight());
        Preferences.setPreference(this.getClass().getSimpleName() + "_" + Preferences.SEARCHWINDOW_X, getX());
        Preferences.setPreference(this.getClass().getSimpleName() + "_" + Preferences.SEARCHWINDOW_Y, getY());

        m_modePanel.savePreferences(this.getClass().getSimpleName() + "_");
        
        // search options
        if (m_removeDupCB != null)
            Preferences.setPreference(this.getClass().getSimpleName() + "_" + Preferences.SEARCHWINDOW_REMOVE_DUP, Boolean.toString(m_removeDupCB.isSelected()));
        if (m_trueMissingCB != null)
            Preferences.setPreference(this.getClass().getSimpleName() + "_" + Preferences.SEARCHWINDOW_TRUE_MISSING, Boolean.toString(m_trueMissingCB.isSelected()));
        // advanced search options
        Preferences.setPreference(this.getClass().getSimpleName() + "_" + Preferences.SEARCHWINDOW_ADVANCED_VISIBLE, Boolean.toString(m_advancedVisible));
        Preferences.setPreference(this.getClass().getSimpleName() + "_" + Preferences.SEARCHWINDOW_NUMBER_OF_RESULTS,((Integer) m_numberOfResults.getValue()));
        Preferences.setPreference(this.getClass().getSimpleName() + "_" + Preferences.SEARCHWINDOW_MAX_TIME,((Integer) m_maxTime.getValue()));
    }

    // //////////////////////////////////////////////////////////////
    // interface for displaying text in viewer

    protected abstract JComponent[] componentsEnabledWhenResults();
    protected abstract JTextComponent[] textFieldsList();
    
	protected boolean englobeWords() { return false; }
	
    protected abstract ProjectOptions getOptions();

	
    /**
     * Show search result for user
     */
    public void displaySearchResult(final Collection<SearchResultEntry> entries) {
        UIThreadsUtil.executeInSwingThread(() -> {
                m_resultsLabel.setText(StringUtil.format(OStrings.getString("SW_DISPLAYING"),entries.size()));
                m_viewer.displaySearchResult(getOptions(), entries, ((Integer) m_numberOfResults.getValue()),
                    Preferences.getPreferenceDefault(getFormatOptionName(), getFormatOptionDefaultValue()), englobeWords());
                if (entries.size() > 0) // else, it is useless
					for (JComponent comp: componentsEnabledWhenResults()) comp.setEnabled(true);
                m_stopSearchButton.setEnabled(false);
            });
    }

    protected void searchEnded(int resNum, int matchesNum) {
		if (resNum == matchesNum) m_resultsLabel.setText(StringUtil.format(OStrings.getString("SW_NR_OF_RESULTS"), resNum));
		else m_resultsLabel.setText(StringUtil.format(OStrings.getString("SW_NR_OF_RESULTS_WITH_MATCHES"), resNum, matchesNum));
        m_searchButton.setEnabled (true); m_formatButton.setEnabled (true);
		long time = System.currentTimeMillis() - m_thread.startTime; 
		if (time < 60000) m_elapsedTimeLabel.setText(StringUtil.format(OStrings.getString("SW_SEARCHING_TIME"), "" + (time / 1000) + "." + String.format("%03d", time % 1000) ) + " s."); 
		else { time /= 1000; m_elapsedTimeLabel.setText(StringUtil.format(OStrings.getString("SW_SEARCHING_TIME"), "" + (time / 60) + ":" + String.format("%02d", time % 60) )); }
    }
    
    // /////////////////////////////////////////////////////////////
    // internal functions

    @Override
    public void processWindowEvent(WindowEvent w) {
        int evt = w.getID();
        if (evt == WindowEvent.WINDOW_CLOSING || evt == WindowEvent.WINDOW_CLOSED) {
            // save user preferences
            savePreferences();

            if (m_thread != null) m_thread.fin();
        }
        super.processWindowEvent(w);
    }


    protected abstract Searcher buildSearcher (Searcher previous) throws IllegalArgumentException;
    
    protected final void doSearch(boolean fromPrevious) {
        UIThreadsUtil.mustBeSwingThread();
        if (m_thread != null) m_thread.fin(); // stop old search thread

        getMainSearchTextField().requestFocus();

        m_viewer.reset(); 
        
        // save user preferences
        savePreferences();

        Object item = getMainSearchTextField().getSelectedItem();
        if ((item == null) || (item.toString().length() == 0))
            setTitle(OStrings.getString("SW_TITLE_SEARCH"));
        else
            setTitle(item.toString() + " - OmegaT");

        // start the search in a separate thread
        try { m_thread = buildSearcher (fromPrevious ? m_thread : null); } 
        catch (java.util.regex.PatternSyntaxException pe) { displayErrorRB(pe, "ST_REGEXP_ERROR"); }
        catch (IllegalArgumentException ie) { displayErrorRB(ie, ie.getLocalizedMessage()); } // display as a text, not as an exception
        catch (Exception e) { displayErrorRB(e, null); Log.log(e); }
        if (m_thread != null) {
            m_searchButton.setEnabled (false); m_stopSearchButton.setEnabled(true);
            m_formatButton.setEnabled (false);
            m_viewer.setText (OStrings.getString("SW_SEARCHING"));
            m_resultsLabel.setText (OStrings.getString("SW_SEARCHING"));
            lastDisplayProgress = 0L;
            m_thread.start();
        }
    }

	private void stopAndDisplay() {
        UIThreadsUtil.mustBeSwingThread();
		m_thread.fin(); 
		Collection<SearchResultEntry> res = m_thread.getSearchResults();
		searchEnded(res.size(), res.size());
        m_searchButton.setEnabled (true); m_stopSearchButton.setEnabled(false);
		displaySearchResult(res);
	}
	
    private void doCancel() {
        UIThreadsUtil.mustBeSwingThread();
        if (m_thread != null) m_thread.fin();
        m_searchButton.setEnabled (true); m_stopSearchButton.setEnabled(false);
        dispose();
    }

    protected void loadAdvancedOptionPreferences() {
        // advanced options visibility
        String advancedVisible = Preferences.getPreferenceDefault(this.getClass().getSimpleName() + "_" + Preferences.SEARCHWINDOW_ADVANCED_VISIBLE, 
            Preferences.getPreferenceDefault(Preferences.SEARCHWINDOW_ADVANCED_VISIBLE, "false"));
        m_advancedVisible = Boolean.valueOf(advancedVisible).booleanValue();

        // Number of results
        m_numberOfResults.setValue(
            Preferences.getPreferenceDefault(this.getClass().getSimpleName() + "_" + Preferences.SEARCHWINDOW_NUMBER_OF_RESULTS, 
                Preferences.getPreferenceDefault(Preferences.SEARCHWINDOW_NUMBER_OF_RESULTS, OConsts.ST_MAX_SEARCH_RESULTS)));
		m_maxTime.setValue(
            Preferences.getPreferenceDefault(this.getClass().getSimpleName() + "_" + Preferences.SEARCHWINDOW_MAX_TIME, 
                Preferences.getPreferenceDefault(Preferences.SEARCHWINDOW_MAX_TIME, 10)));
    }

    protected void updateAdvancedOptionStatus() {
        advancedPanel.setVisible(m_advancedVisible);
        Mnemonics.setLocalizedText(m_advancedButton, OStrings.getString(advancedPanel.isVisible() ? "SW_HIDE_ADVANCED_OPTIONS" : "SW_SHOW_ADVANCED_OPTIONS"));
    }

    /**
     * Display error message in RED inside entry result pane
     * 
     * @param ex
     *            exception to show
     * @param errorKey
     *            error message key in resource bundle
     * @param params
     *            error text parameters
     */
    public void displayErrorRB(final Throwable ex, final String errorKey, final Object... params) {
        UIThreadsUtil.executeInSwingThread(() -> {
            try {
                DefaultStyledDocument doc = new DefaultStyledDocument();
                doc.insertString(0, OStrings.getString("LD_ERROR").replace("{0}", ""), Styles.createAttributeSet(Color.RED, null, true, null));
                if (errorKey != null) { // display error key in red, details exception in black
                    String msg = errorKey; try { msg = OStrings.getString(errorKey); if (params != null) msg = StringUtil.format(msg, params); } catch (Exception e0) {}
                    doc.insertString(OStrings.getString("LD_ERROR").length() - 3, msg, Styles.createAttributeSet(Color.RED, null, false, null));
                    
                    if (ex != null) {
                        doc.insertString(doc.getLength(), "\n\n", Styles.createAttributeSet(Color.BLACK, null, false, null));
                        doc.insertString(doc.getLength(), ex.getLocalizedMessage() + "\n", Styles.createAttributeSet(Color.BLACK, null, false, null));
                    }
                } else { // displays exception message in red followed by details in black
                    doc.insertString(OStrings.getString("LD_ERROR").length() - 3, ex.getLocalizedMessage(), Styles.createAttributeSet(Color.RED, null, false, null));                    
                }
                java.io.StringWriter stackBuf = new java.io.StringWriter(); ex.printStackTrace(new java.io.PrintWriter(stackBuf));
                doc.insertString(doc.getLength(), stackBuf.toString(), Styles.createAttributeSet(Color.BLACK, null, false, null));
                
                m_viewer.setDocument(doc);
            } catch (Exception e2) {
                m_viewer.setText(String.format(OStrings.getString("LD_ERROR"), "") + "\n\n" 
                  + ((errorKey != null) ? StringUtil.format(OStrings.getString(errorKey), params) : "")
		  + ((ex != null) ? ex.getMessage() : ""));
            }
                
                m_searchButton.setEnabled (true);
                m_formatButton.setEnabled (true);
        });
    }
	
	private long lastDisplayProgress = 0L;
	
	/** Display progress (not an error) **/
	public void displayProgress(String step, int done, int total, int found) {
		long time = System.currentTimeMillis() - m_thread.startTime; 
		if (time < 1000) return; // do not slow down fast queries
		if (time - lastDisplayProgress < 1000) return; lastDisplayProgress = time;
		UIThreadsUtil.executeInSwingThread(() -> {
			long localTime = time / 1000;
			if (localTime > (60L * (Integer) m_maxTime.getValue())) this.stopAndDisplay();
			else
				m_viewer.setText (
                    StringUtil.format(OStrings.getString("SW_SEARCHING_PROGRESS"), 
                        step.contains(":") ? (
                              OStrings.getString("SW_SEARCHING_STEP_" + step.substring(0, step.indexOf(":"))) + ", "
                            + OStrings.getString("SW_SEARCHING_STEP_FILE") + " " + step.substring(step.indexOf(":") + 1)
                        ) : OStrings.getString("SW_SEARCHING_STEP_" + step)
                    )
					+ " (" + done + "/" + total + " = " + (int) (100.0 * done / total) + " %)\n"
					+ StringUtil.format(OStrings.getString("SW_SEARCHING_FOUND"), found) + "\n"
					+ StringUtil.format(OStrings.getString("SW_SEARCHING_TIME"), "" + (localTime / 60) + ":" + String.format("%02d", localTime % 60) ) 
				);				
		});
	}
	

	protected static class PopupFactory extends MouseAdapter {
        private javax.swing.text.JTextComponent receiver;
		protected org.omegat.util.MultiMap<Integer, IPopupMenuConstructor> popupConstructors;
		
		public PopupFactory (javax.swing.text.JTextComponent receiver) {
			this.receiver = receiver;
			this.popupConstructors = new org.omegat.util.MultiMap<>(true);
			this.popupConstructors.put (400, new org.omegat.gui.editor.EditorPopups.DefaultPopup());
			this.popupConstructors.put (700, new org.omegat.gui.editor.EditorPopups.InsertTagsPopup());
		}
		
		@Override
        public void mouseClicked(MouseEvent e) {
            if (e.isPopupTrigger() || e.getButton() == MouseEvent.BUTTON3) {
                javax.swing.JPopupMenu popup = makePopupMenu(receiver.viewToModel(e.getPoint()));
                if (popup.getComponentCount() > 0)
                    popup.show(receiver, (int) e.getPoint().getX(), (int) e.getPoint().getY());
            }
        }

		private javax.swing.JPopupMenu makePopupMenu(int pos) {
			java.util.List<IPopupMenuConstructor> cons = new java.util.LinkedList<>();
			synchronized (popupConstructors) {
				// Copy constructors - for disable blocking in the procesing time. 
				for (IPopupMenuConstructor cons0: popupConstructors.values()) cons.add(cons0);
			}


			javax.swing.JPopupMenu popup = new javax.swing.JPopupMenu();
			for (IPopupMenuConstructor c : cons) 
				c.addItems(popup, receiver, pos, false, true, null);

			org.omegat.util.gui.DockingUI.removeUnusedMenuSeparators(popup);

			return popup;			
		}
	}
	
    protected static class SetRef {
        public Set<String> theSet;
        
        public SetRef (Set<String> theSet) { this.theSet = theSet; }
        
        public void add (String val) { theSet.add (val); }
    }

    /** Creates a button "Memorize" and the necessary listeners. **/
    protected JButton createMemorizeButton (final int winY, final boolean canAll, final JComboBox field, final JComboBox replace, final String checkLabel, final SetRef valuesSet) {
        final JButton memoBtn = new JButton (OStrings.getString("SW_SEARCH_MEMORIZE"));
        memoBtn.addActionListener (e -> {
                final javax.swing.JDialog dialog = new javax.swing.JDialog();
                dialog.setTitle("Select scope"); dialog.setModal(true);
                class ButtonListener implements ActionListener {
                    private String destDir;
                    private boolean cancel = false, global = false;
                    
                    public ButtonListener (boolean cancel, boolean global, String destDir) { 
                        this.destDir = destDir; this.cancel = cancel; this.global = global;
                    }
                    
                    public void actionPerformed (ActionEvent e) { 
                        dialog.setVisible(false); if (this.cancel) return;
                        String selection = field.getSelectedItem().toString();
                        field.addItem (selection); valuesSet.add (selection);
                        if (destDir != null) 
                            try {
                                PrintWriter out = new PrintWriter(new BufferedWriter(new FileWriter(destDir + "search.tsv", true)));
                                StringBuffer values = new StringBuffer();
                                if (global) {
                                    values.append(Core.getProject().getProjectProperties().getSourceLanguage()).append("\t");
                                    values.append(Core.getProject().getProjectProperties().getTargetLanguage()).append("\t");
                                }
                                if ("REGEXP".equals(m_modePanel.searchTypeString())) {
                                    values.append ("*").append("\t");	// common to all windows
									if (checkLabel.contains("SEARCH")) values.append ("SW_SEARCH_*").append(":");	// any search screen
									else values.append (checkLabel).append(":");	// replace is not a regex
                                    values.append (m_modePanel.searchTypeString()).append("\t");
                                } else {
                                    values.append (SearchWindow.this.getClass().getSimpleName()).append("\t");
                                    values.append (checkLabel).append(":");
                                    values.append (m_modePanel.searchTypeString()).append("\t");
                                }
                                values.append ("{").append(m_modePanel.wordModeString()).append("}");
                                values.append (selection);
                                if (replace != null) {
                                    String replaceTxt = replace.getSelectedItem().toString();
                                    values.append("\t").append(replaceTxt);
                                    if (selection == null) return;
                                }
                                out.println (values.toString());
                                out.close();
                            } catch (Exception ex) {
                                ex.printStackTrace();
                            }				
                    }
                }
                JButton bSession = new JButton ("For this session"), bProj = new JButton ("For the project"), bAll = new JButton ("For all projects"), bCancel=new JButton("Cancel");
                bCancel.addActionListener (new ButtonListener(true,false,null));
                bSession.addActionListener (new ButtonListener(false,false,null));
                bProj.addActionListener (new ButtonListener(false,false,Core.getProject().getProjectProperties().getProjectRoot()));
                if (canAll) bAll.addActionListener (new ButtonListener(false,true,org.omegat.util.StaticUtils.getConfigDir()));
                Container cp = dialog.getContentPane(); cp.setLayout(new GridLayout (4,1));
                cp.add(bSession); cp.add(bProj); if (canAll) cp.add(bAll); cp.add(bCancel);
                dialog.pack(); dialog.setLocation(memoBtn.getX() - 50, winY); dialog.setVisible(true);		
            });
        return memoBtn;
    }
    
    /** Used by subclasses to pre-load searches when we create the window **/
    static class SearchesLoader<SC> implements IProjectEventListener {
        private Map<String, SC> asso;
        private String name;
        
        public SearchesLoader (String name, Map<String, SC> asso) {
            this.asso = asso; this.name = name;

            load(true);
            if (Core.getProject() != null)
                if (Core.getProject().getProjectProperties() != null)
                    load(false);
        }
    
        public void onProjectChanged(PROJECT_CHANGE_TYPE type) {
            if ((type == IProjectEventListener.PROJECT_CHANGE_TYPE.COMPILE) || (type == IProjectEventListener.PROJECT_CHANGE_TYPE.SAVE)) return;
            
            for (Object set: asso.values()) 
                if (set instanceof Set) ((Set) set).clear();
                else if (set instanceof Map) ((Map) set).clear();
            if (type == IProjectEventListener.PROJECT_CHANGE_TYPE.CLOSE) return;
            load(true);
            if (Core.getProject() != null)
                if (Core.getProject().getProjectProperties() != null)
                    load(false);
        }
        
        private void load(boolean global) {
            try {
                String fileName = 
                    global ? org.omegat.util.StaticUtils.getConfigDir() + "search.tsv"
                           : Core.getProject().getProjectProperties().getProjectRoot() + "search.tsv";
                Log.log ("Loading searches from " + fileName);
                java.io.BufferedReader reader = new java.io.BufferedReader (new java.io.FileReader(fileName));
                int classPos = 0, typePos = 1, dataPos = 2; if (global) { classPos += 2; typePos += 2; dataPos += 2; }
                String line;
                while ((line = reader.readLine()) != null) {
                    String[] cols = line.split("\t");
                    if (! (cols[classPos].equals("*") || cols[classPos].equals(name))) continue;
                    if (global) {
                        if (cols[0].equals("*")) cols[0] = Core.getProject().getProjectProperties().getSourceLanguage().toString();
                        if (cols[1].equals("*")) cols[1] = Core.getProject().getProjectProperties().getTargetLanguage().toString();
                        
                        if (! cols[0].equals(Core.getProject().getProjectProperties().getSourceLanguage().toString())) continue;
                        if (! cols[1].equals(Core.getProject().getProjectProperties().getTargetLanguage().toString())) continue;
                    }
                    
                    Collection<SC> typeSets = java.util.Collections.emptySet();
                    if (! cols[typePos].contains("*")) typeSets = java.util.Collections.singleton(asso.get (cols[typePos]));
                    else if (cols[typePos].equals("*:*")) typeSets = asso.values();
                    else if (cols[typePos].startsWith("*:")) {
                        typeSets = new java.util.ArrayList<SC>();
                        for (Map.Entry<String,SC> me: asso.entrySet())
                            if (me.getKey().endsWith(cols[typePos].substring(cols[typePos].indexOf(':') + 1))) typeSets.add (me.getValue());
                    }
                    else if (cols[typePos].startsWith("SW_SEARCH_*:")) {
                        typeSets = new java.util.ArrayList<SC>();
                        for (Map.Entry<String,SC> me: asso.entrySet())
                            if (me.getKey().startsWith("SW_SEARCH") && me.getKey().endsWith(cols[typePos].substring(cols[typePos].indexOf(':') + 1))) typeSets.add (me.getValue());
                    }
                    else if (cols[typePos].endsWith(":*")) {
                        typeSets = new java.util.ArrayList<SC>();
                        for (Map.Entry<String,SC> me: asso.entrySet())
                            if (me.getKey().startsWith(cols[typePos].substring(0, cols[typePos].indexOf(':')))) typeSets.add (me.getValue());	
                    }
                    for (SC dest: typeSets)
                        if (dest instanceof Set) ((Set<String>) dest).add (cols[dataPos]);
                        else if (dest instanceof Map) {
                            Map<String, Set<String>> searches = (Map<String, Set<String>>) dest;
                            Set<String> localAsso = searches.get (cols[dataPos]);
                            if (localAsso == null) searches.put (cols[dataPos], localAsso = new java.util.TreeSet<String>());
                            localAsso.add (cols[dataPos + 1]);
                        }
                }
                reader.close();
            } catch (java.io.FileNotFoundException fnf) {
                Log.log ("File does not exist. Nothing to read.");
            } catch (Exception ex) {
                Log.log(ex);
            }
        }
            
    
    }
	
	class RegexModeSwitchListener implements ActionListener {
		private SearchModeBox box;
		private org.omegat.util.gui.RegexHighlightListener listenerRegex, listenerJokers;
		private javax.swing.text.JTextComponent receiver;
		
		public RegexModeSwitchListener(SearchModeBox box, javax.swing.text.JTextComponent receiver) {
			this.box = box; this.receiver = receiver;
			this.listenerRegex = new org.omegat.util.gui.RegexHighlightListener(receiver, org.omegat.util.gui.RegexHighlightListener.MODE_REGEX_FIND);
			this.listenerJokers = new org.omegat.util.gui.RegexHighlightListener(receiver, org.omegat.util.gui.RegexHighlightListener.MODE_NON_REGEX);
			this.actionPerformed(null);
		}
		
		public void actionPerformed (ActionEvent ev) {
			if (box.searchTypeString().equals("REGEXP")) {
				receiver.getDocument().addDocumentListener (listenerRegex); receiver.getDocument().removeDocumentListener (listenerJokers);	
			} else {
				receiver.getDocument().removeDocumentListener (listenerRegex); receiver.getDocument().addDocumentListener (listenerJokers);			
			}
		}
	}
    
    private JButton m_searchButton, m_stopSearchButton;
    private JButton m_formatButton;

    protected SearchModeBox m_modePanel;
    
    protected JLabel m_resultsLabel;
    private JButton m_advancedButton;

    protected boolean m_advancedVisible;
    protected JComponent advancedPanel;

    protected JSpinner m_numberOfResults, m_maxTime;
	private JLabel m_elapsedTimeLabel;

    protected JCheckBox m_removeDupCB, m_trueMissingCB;
    
    protected EntryListPane m_viewer;

    protected Searcher m_thread;
    
    public final static int H_MARGIN = 10;
}
