/*****************************************************************************
 * Copyright (c) 2016, 2020 Ericsson
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License 2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *****************************************************************************/
package org.eclipse.tracecompass.tmf.ui.views.timegraph;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;

import org.eclipse.jdt.annotation.NonNull;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jface.action.LegacyActionTools;
import org.eclipse.jface.dialogs.Dialog;
import org.eclipse.jface.dialogs.IDialogSettings;
import org.eclipse.jface.fieldassist.ComboContentAdapter;
import org.eclipse.jface.text.FindReplaceDocumentAdapterContentProposalProvider;
import org.eclipse.jface.util.Util;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.ModifyEvent;
import org.eclipse.swt.events.ModifyListener;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.events.SelectionListener;
import org.eclipse.swt.events.ShellAdapter;
import org.eclipse.swt.events.ShellEvent;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Combo;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Group;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.tracecompass.common.core.NonNullUtils;
import org.eclipse.tracecompass.internal.tmf.ui.Activator;
import org.eclipse.tracecompass.internal.tmf.ui.Messages;
import org.eclipse.tracecompass.tmf.ui.views.timegraph.AbstractTimeGraphView.FindTarget;
import org.eclipse.tracecompass.tmf.ui.widgets.timegraph.model.ITimeGraphEntry;
import org.eclipse.ui.fieldassist.ContentAssistCommandAdapter;
import org.eclipse.ui.texteditor.ITextEditorActionDefinitionIds;

import com.google.common.collect.BiMap;
import com.google.common.collect.HashBiMap;

/**
 * Find dialog to search entries into a time graph. This implementation is based
 * on the org.eclipse.ui.texteditor.FindReplaceDialog of Eclipse.
 *
 * @author Jean-Christian Kouame
 */
class TimeGraphFindDialog extends Dialog {

    /**
     * Updates the find dialog on activation changes.
     */
    class ActivationListener extends ShellAdapter {
        /*
         * @see ShellListener#shellActivated(ShellEvent)
         */
        @Override
        public void shellActivated(ShellEvent e) {
            fActiveShell = (Shell) e.widget;
            updateButtonState();

            if (fGiveFocusToFindField && getShell() == fActiveShell && okToUse(fFindField)) {
                fFindField.setFocus();
            }

        }

        /*
         * @see ShellListener#shellDeactivated(ShellEvent)
         */
        @Override
        public void shellDeactivated(ShellEvent e) {
            fGiveFocusToFindField = false;

            storeSettings();

            fActiveShell = null;
            updateButtonState();
        }
    }

    /**
     * Modify listener to update the search result in case of incremental
     * search.
     */
    private class FindModifyListener implements ModifyListener {

        /*
         * @see ModifyListener#modifyText(ModifyEvent)
         */
        @Override
        public void modifyText(ModifyEvent e) {
            updateButtonState();
        }
    }

    /** The size of the dialogs search history. */
    private static final int HISTORY_SIZE = 5;

    private final String WRAP = "wrap"; //$NON-NLS-1$
    private final String CASE_SENSITIVE = "casesensitive"; //$NON-NLS-1$
    private final String WHOLE_WORD = "wholeword"; //$NON-NLS-1$
    private final String IS_REGEX_EXPRESSION = "isRegEx"; //$NON-NLS-1$
    private final String FIND_HISTORY = "findhistory"; //$NON-NLS-1$

    private boolean fWrapInit;
    private boolean fCaseInit;
    private boolean fWholeWordInit;
    private boolean fForwardInit;
    private boolean fIsRegExInit;

    private @NonNull List<String> fFindHistory;

    private static Shell fParentShell;
    private Shell fActiveShell;

    private final ActivationListener fActivationListener = new ActivationListener();
    private final FindModifyListener fFindModifyListener = new FindModifyListener();

    private Label fStatusLabel;
    private Button fForwardRadioButton;
    private Button fCaseCheckBox;
    private Button fWrapCheckBox;
    private Button fWholeWordCheckBox;
    private Button fIsRegExCheckBox;

    private Button fFindNextButton;
    private Combo fFindField;

    /**
     * Find command adapters.
     */
    private ContentAssistCommandAdapter fContentAssistFindField;

    private Rectangle fDialogPositionInit;

    private IDialogSettings fDialogSettings;
    /**
     * <code>true</code> if the find field should receive focus the next time
     * the dialog is activated, <code>false</code> otherwise.
     */
    private boolean fGiveFocusToFindField = true;

    /**
     * Holds the mnemonic/button pairs for all buttons.
     */
    private HashMap<Character, Button> fMnemonicButtonMap = new HashMap<>();

    private @Nullable FindTarget fFindTarget;

    /**
     * Creates a new dialog with the given shell as parent.
     *
     * @param parentShell
     *            the parent shell
     */
    public TimeGraphFindDialog(Shell parentShell) {
        super(parentShell);

        fParentShell = null;
        fFindTarget = null;

        fDialogPositionInit = null;
        fFindHistory = new ArrayList<>(HISTORY_SIZE - 1);

        fWrapInit = true;
        fCaseInit = false;
        fIsRegExInit = false;
        fWholeWordInit = false;
        fForwardInit = true;

        readConfiguration();
        setShellStyle(getShellStyle() & ~SWT.APPLICATION_MODAL);
        setBlockOnOpen(false);
    }

    @Override
    protected boolean isResizable() {
        return true;
    }

    /**
     * Returns <code>true</code> if control can be used.
     *
     * @param control
     *            the control to be checked
     * @return <code>true</code> if control can be used
     */
    private static boolean okToUse(Control control) {
        return control != null && !control.isDisposed();
    }

    @Override
    public void create() {
        super.create();

        Shell shell = getShell();
        shell.addShellListener(fActivationListener);

        // fill in combo contents
        fFindField.removeModifyListener(fFindModifyListener);
        updateCombo(fFindField, fFindHistory);
        fFindField.addModifyListener(fFindModifyListener);

        // get find string
        initFindStringFromSelection();

        shell.setMinimumSize(shell.getSize());

        // set dialog position
        if (fDialogPositionInit != null) {
            shell.setBounds(fDialogPositionInit);
        }

        shell.setText(Messages.TimeGraphFindDialog_FindTitle);
    }

    /**
     * Creates the options configuration section of the find dialog.
     *
     * @param parent
     *            the parent composite
     * @return the options configuration section
     */
    private Composite createConfigPanel(Composite parent) {

        Composite panel = new Composite(parent, SWT.NONE);
        GridLayout layout = new GridLayout();
        panel.setLayout(layout);

        Composite directionGroup = createDirectionGroup(panel);
        setGridData(directionGroup, SWT.FILL, true, SWT.FILL, false);

        Composite optionsGroup = createOptionsGroup(panel);
        setGridData(optionsGroup, SWT.FILL, true, SWT.FILL, true);
        ((GridData) optionsGroup.getLayoutData()).horizontalSpan = 2;

        return panel;
    }

    @Override
    protected Control createContents(Composite parent) {

        Composite panel = new Composite(parent, SWT.NULL);
        GridLayout layout = new GridLayout();
        layout.numColumns = 1;
        layout.makeColumnsEqualWidth = true;
        panel.setLayout(layout);
        panel.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));

        Composite inputPanel = createInputPanel(panel);
        setGridData(inputPanel, SWT.FILL, true, SWT.TOP, false);

        Composite configPanel = createConfigPanel(panel);
        setGridData(configPanel, SWT.FILL, true, SWT.TOP, true);

        Composite statusBar = createStatusAndCloseButton(panel);
        setGridData(statusBar, SWT.FILL, true, SWT.BOTTOM, false);

        panel.addTraverseListener(e -> {
            if (e.detail == SWT.TRAVERSE_RETURN) {
                if (!Util.isMac()) {
                    Control controlWithFocus = getShell().getDisplay().getFocusControl();
                    if (controlWithFocus != null && (controlWithFocus.getStyle() & SWT.PUSH) == SWT.PUSH) {
                        return;
                    }
                }
                Event event1 = new Event();
                event1.type = SWT.Selection;
                event1.stateMask = e.stateMask;
                fFindNextButton.notifyListeners(SWT.Selection, event1);
                e.doit = false;
            } else if (e.detail == SWT.TRAVERSE_MNEMONIC) {
                Character mnemonic = Character.valueOf(Character.toLowerCase(e.character));
                Button button = fMnemonicButtonMap.get(mnemonic);
                if (button != null) {
                    if ((fFindField.isFocusControl() || (button.getStyle() & SWT.PUSH) != 0)
                            && button.isEnabled()) {
                        Event event2 = new Event();
                        event2.type = SWT.Selection;
                        event2.stateMask = e.stateMask;
                        if ((button.getStyle() & SWT.RADIO) != 0) {
                            Composite buttonParent = button.getParent();
                            if (buttonParent != null) {
                                Control[] children = buttonParent.getChildren();
                                for (int i = 0; i < children.length; i++) {
                                    ((Button) children[i]).setSelection(false);
                                }
                            }
                            button.setSelection(true);
                        } else {
                            button.setSelection(!button.getSelection());
                        }
                        button.notifyListeners(SWT.Selection, event2);
                        e.detail = SWT.TRAVERSE_NONE;
                        e.doit = true;
                    }
                }
            }
        });

        updateButtonState();

        applyDialogFont(panel);

        return panel;
    }

    private void setContentAssistsEnablement(boolean enable) {
        fContentAssistFindField.setEnabled(enable);
    }

    /**
     * Creates the direction defining part of the options defining section of
     * the find dialog.
     *
     * @param parent
     *            the parent composite
     * @return the direction defining part
     */
    private Composite createDirectionGroup(Composite parent) {

        Composite panel = new Composite(parent, SWT.NONE);
        GridLayout layout = new GridLayout();
        layout.marginWidth = 0;
        layout.marginHeight = 0;
        panel.setLayout(layout);

        Group group = new Group(panel, SWT.SHADOW_ETCHED_IN);
        group.setText(Messages.TimeGraphFindDialog_Direction);
        GridLayout groupLayout = new GridLayout();
        group.setLayout(groupLayout);
        group.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));

        fForwardRadioButton = new Button(group, SWT.RADIO | SWT.LEFT);
        fForwardRadioButton.setText(Messages.TimeGraphFindDialog_ForwardRadioButtonLabel);
        setGridData(fForwardRadioButton, SWT.LEFT, false, SWT.CENTER, false);
        storeButtonWithMnemonicInMap(fForwardRadioButton);

        Button backwardRadioButton = new Button(group, SWT.RADIO | SWT.LEFT);
        backwardRadioButton.setText(Messages.TimeGraphFindDialog_BackwardRadioButtonLabel);
        setGridData(backwardRadioButton, SWT.LEFT, false, SWT.CENTER, false);
        storeButtonWithMnemonicInMap(backwardRadioButton);

        backwardRadioButton.setSelection(!fForwardInit);
        fForwardRadioButton.setSelection(fForwardInit);

        return panel;
    }

    /**
     * Creates the panel where the user specifies the text to search for
     *
     * @param parent
     *            the parent composite
     * @return the input panel
     */
    private Composite createInputPanel(Composite parent) {
        Composite panel = new Composite(parent, SWT.NULL);
        GridLayout layout = new GridLayout();
        layout.numColumns = 2;
        panel.setLayout(layout);

        Label findLabel = new Label(panel, SWT.LEFT);
        findLabel.setText(Messages.TimeGraphFindDialog_FindLabel);
        setGridData(findLabel, SWT.LEFT, false, SWT.CENTER, false);

        // Create the find content assist field
        ComboContentAdapter contentAdapter = new ComboContentAdapter();
        FindReplaceDocumentAdapterContentProposalProvider findProposer = new FindReplaceDocumentAdapterContentProposalProvider(true);
        fFindField = new Combo(panel, SWT.DROP_DOWN | SWT.BORDER);
        fContentAssistFindField = new ContentAssistCommandAdapter(
                fFindField,
                contentAdapter,
                findProposer,
                ITextEditorActionDefinitionIds.CONTENT_ASSIST_PROPOSALS,
                new char[0],
                true);
        setGridData(fFindField, SWT.FILL, true, SWT.CENTER, false);
        fFindField.addModifyListener(fFindModifyListener);

        return panel;
    }

    /**
     * Creates the functional options part of the options defining section of
     * the find dialog.
     *
     * @param parent
     *            the parent composite
     * @return the options group
     */
    private Composite createOptionsGroup(Composite parent) {

        Composite panel = new Composite(parent, SWT.NONE);
        GridLayout layout = new GridLayout();
        layout.marginWidth = 0;
        layout.marginHeight = 0;
        panel.setLayout(layout);

        Group group = new Group(panel, SWT.SHADOW_NONE);
        group.setText(Messages.TimeGraphFindDialog_Options);
        GridLayout groupLayout = new GridLayout();
        groupLayout.numColumns = 1;
        groupLayout.makeColumnsEqualWidth = true;
        group.setLayout(groupLayout);
        group.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));

        SelectionListener selectionListener = new SelectionListener() {
            @Override
            public void widgetSelected(SelectionEvent e) {
                storeSettings();
            }

            @Override
            public void widgetDefaultSelected(SelectionEvent e) {
                // Do nothing
            }
        };

        fCaseCheckBox = new Button(group, SWT.CHECK | SWT.LEFT);
        fCaseCheckBox.setText(Messages.TimeGraphFindDialog_CaseCheckBoxLabel);
        setGridData(fCaseCheckBox, SWT.LEFT, false, SWT.CENTER, false);
        fCaseCheckBox.setSelection(fCaseInit);
        fCaseCheckBox.addSelectionListener(selectionListener);
        storeButtonWithMnemonicInMap(fCaseCheckBox);

        fWrapCheckBox = new Button(group, SWT.CHECK | SWT.LEFT);
        fWrapCheckBox.setText(Messages.TimeGraphFindDialog_WrapCheckBoxLabel);
        setGridData(fWrapCheckBox, SWT.LEFT, false, SWT.CENTER, false);
        fWrapCheckBox.setSelection(fWrapInit);
        fWrapCheckBox.addSelectionListener(selectionListener);
        storeButtonWithMnemonicInMap(fWrapCheckBox);

        fWholeWordCheckBox = new Button(group, SWT.CHECK | SWT.LEFT);
        fWholeWordCheckBox.setText(Messages.TimeGraphFindDialog_WholeWordCheckBoxLabel);
        setGridData(fWholeWordCheckBox, SWT.LEFT, false, SWT.CENTER, false);
        fWholeWordCheckBox.setSelection(fWholeWordInit);
        fWholeWordCheckBox.addSelectionListener(selectionListener);
        storeButtonWithMnemonicInMap(fWholeWordCheckBox);

        fIsRegExCheckBox = new Button(group, SWT.CHECK | SWT.LEFT);
        fIsRegExCheckBox.setText(Messages.TimeGraphFindDialog_REgExCheckBoxLabel);
        setGridData(fIsRegExCheckBox, SWT.LEFT, false, SWT.CENTER, false);
        ((GridData) fIsRegExCheckBox.getLayoutData()).horizontalSpan = 2;
        fIsRegExCheckBox.setSelection(fIsRegExInit);
        fIsRegExCheckBox.addSelectionListener(new SelectionAdapter() {
            @Override
            public void widgetSelected(SelectionEvent e) {
                boolean newState = fIsRegExCheckBox.getSelection();
                updateButtonState();
                storeSettings();
                setContentAssistsEnablement(newState);
            }
        });
        storeButtonWithMnemonicInMap(fIsRegExCheckBox);
        fWholeWordCheckBox.setEnabled(!isRegExSearch());
        fWholeWordCheckBox.addSelectionListener(new SelectionAdapter() {
            @Override
            public void widgetSelected(SelectionEvent e) {
                updateButtonState();
            }
        });
        return panel;
    }

    /**
     * Creates the status and close section of the dialog.
     *
     * @param parent
     *            the parent composite
     * @return the status and close button
     */
    private Composite createStatusAndCloseButton(Composite parent) {

        Composite panel = new Composite(parent, SWT.NULL);
        GridLayout layout = new GridLayout();
        layout.numColumns = 2;
        layout.marginWidth = 0;
        layout.marginHeight = 0;
        panel.setLayout(layout);

        fStatusLabel = new Label(panel, SWT.LEFT);
        fStatusLabel.setText(Messages.TimeGraphFindDialog_StatusWrappedLabel);
        setGridData(fStatusLabel, SWT.FILL, true, SWT.CENTER, false);
        GridData gd = (GridData) fStatusLabel.getLayoutData();
        gd.widthHint = fStatusLabel.computeSize(SWT.DEFAULT, SWT.DEFAULT).x;
        fStatusLabel.setText("");  //$NON-NLS-1$

        Composite buttonSection = new Composite(panel, SWT.NULL);
        GridLayout buttonLayout = new GridLayout();
        buttonLayout.numColumns = 2;
        buttonSection.setLayout(buttonLayout);

        String label = Messages.TimeGraphFindDialog_CloseButtonLabel;
        Button closeButton = createButton(buttonSection, 101, label, false);
        setGridData(closeButton, SWT.RIGHT, false, SWT.BOTTOM, false);

        fFindNextButton = makeButton(buttonSection, Messages.TimeGraphFindDialog_FindNextButtonLabel, 102, true, new SelectionAdapter() {
            @Override
            public void widgetSelected(SelectionEvent e) {
                performSearch(((e.stateMask & SWT.SHIFT) != 0) ^ isForwardSearch());
                updateFindHistory();
            }
        });
        setGridData(fFindNextButton, SWT.FILL, true, SWT.FILL, false);

        return panel;
    }

    @Override
    protected void buttonPressed(int buttonID) {
        if (buttonID == 101) {
            close();
        }
    }

    /**
     * Update the dialog data (parentShell, listener, input, ...)
     *
     * @param findTarget
     *            the new find target
     */
    public void update(@NonNull FindTarget findTarget) {
        updateTarget(findTarget, true);
    }

    // ------- action invocation ---------------------------------------

    /**
     * Returns the index of the entry that match the specified search string, or
     * <code>-1</code> if the string can not be found when searching using the
     * given options.
     *
     * @param findString
     *            the string to search for
     * @param startIndex
     *            the index at which to start the search
     * @param items
     *            The map of items in the time graph view
     * @param options
     *            The options use for the search
     * @return the index of the find entry following the options or
     *         <code>-1</code> if nothing found
     */
    private int findNext(String findString, int startIndex, BiMap<ITimeGraphEntry, Integer> items, SearchOptions options) {
        int index;
        if (options.forwardSearch) {
            index = startIndex == items.size() - 1 ? -1 : findNext(startIndex + 1, findString, items, options);
        } else {
            index = startIndex == 0 ? -1 : findNext(startIndex - 1, findString, items, options);
        }

        if (index == -1) {
            if (okToUse(getShell())) {
                getShell().getDisplay().beep();
            }
            if (options.wrapSearch) {
                statusMessage(Messages.TimeGraphFindDialog_StatusWrappedLabel);
                index = findNext(-1, findString, items, options);
            }
        }
        return index;
    }

    private int findNext(int startIndex, String findString, BiMap<ITimeGraphEntry, Integer> items, SearchOptions options) {
        FindTarget findTarget = fFindTarget;
        if (findTarget != null) {
            if (findString == null || findString.length() == 0) {
                return -1;
            }

            final @NonNull Pattern pattern = getPattern(findString, options);
            int index = adjustIndex(startIndex, items.size(), options.forwardSearch);
            BiMap<Integer, ITimeGraphEntry> entries = items.inverse();
            while (index >= 0 && index < entries.size()) {
                final @Nullable ITimeGraphEntry entry = entries.get(index);
                if (entry != null) {
                    String[] columnTexts = findTarget.getColumnTexts(entry);
                    for (int i = 0; i < columnTexts.length; i++) {
                        String columnText = columnTexts[i];
                        if (columnText != null && !columnText.isEmpty() && pattern.matcher(columnTexts[i]).find()) {
                            return index;
                        }
                    }
                }
                index = options.forwardSearch ? ++index : --index;
            }
        }
        return -1;
    }

    /**
     * Returns whether the specified search string can be found using the given
     * options.
     *
     * @param findString
     *            the string to search for
     * @param options
     *            The search options
     * @return <code>true</code> if the search string can be found using the
     *         given options
     *
     */
    private boolean findAndSelect(String findString, SearchOptions options) {
        FindTarget findTarget = fFindTarget;
        if (findTarget == null) {
            return false;
        }
        ITimeGraphEntry[] topInput = findTarget.getEntries();
        BiMap<@NonNull ITimeGraphEntry, @NonNull Integer> items = HashBiMap.create();
        for (ITimeGraphEntry entry : topInput) {
            listEntries(items, entry);
        }
        int startPosition = findTarget.getSelection() == null ? 0 : NonNullUtils.checkNotNull(items.get(findTarget.getSelection()));

        int index = findNext(findString, startPosition, items, options);

        if (index == -1) {
            statusMessage(Messages.TimeGraphFindDialog_StatusNoMatchLabel);
            return false;
        }

        if (options.forwardSearch && index >= startPosition || !options.forwardSearch && index <= startPosition) {
            statusMessage(""); //$NON-NLS-1$
        }

        // Send the entry found to target
        findTarget.selectAndReveal(NonNullUtils.checkNotNull(items.inverse().get(index)));
        return true;
    }

    private void listEntries(Map<ITimeGraphEntry, Integer> items, ITimeGraphEntry root) {
        items.put(root, items.size());
        for (ITimeGraphEntry child : root.getChildren()) {
            listEntries(items, child);
        }
    }

    // ------- accessors ---------------------------------------

    /**
     * Retrieves the string to search for from the appropriate text input field
     * and returns it.
     *
     * @return the search string
     */
    private String getFindString() {
        if (okToUse(fFindField)) {
            return fFindField.getText();
        }
        return ""; //$NON-NLS-1$
    }

    /**
     * Returns the dialog's boundaries.
     *
     * @return the dialog's boundaries
     */
    private Rectangle getDialogBoundaries() {
        if (okToUse(getShell())) {
            return getShell().getBounds();
        }
        return fDialogPositionInit;
    }

    // ------- init / close ---------------------------------------
    @Override
    public boolean close() {
        handleDialogClose();
        return super.close();
    }

    /**
     * Removes focus changed listener from browser and stores settings for
     * re-open.
     */
    private void handleDialogClose() {

        // remove listeners
        if (okToUse(fFindField)) {
            fFindField.removeModifyListener(fFindModifyListener);
        }
        updateTarget(null, false);

        if (fParentShell != null) {
            fParentShell.removeShellListener(fActivationListener);
            fParentShell = null;
        }

        if (getShell() != null && !getShell().isDisposed()) {
            getShell().removeShellListener(fActivationListener);
        }

        // store current settings in case of re-open
        storeSettings();

        // prevent leaks
        fActiveShell = null;

    }

    /**
     * Writes the current selection to the dialog settings.
     */
    private void writeSelection() {
        final FindTarget input = fFindTarget;
        if (input == null) {
            return;
        }

        IDialogSettings s = getDialogSettings();
        final ITimeGraphEntry selection = input.getSelection();
        if (selection != null) {
            s.put("selection", selection.getName()); //$NON-NLS-1$
        }
    }

    /**
     * Stores the current state in the dialog settings.
     */
    private void storeSettings() {
        fDialogPositionInit = getDialogBoundaries();
        fWrapInit = isWrapSearch();
        fWholeWordInit = isWholeWordSetting();
        fCaseInit = isCaseSensitiveSearch();
        fIsRegExInit = isRegExSearch();
        fForwardInit = isForwardSearch();

        writeConfiguration();
    }

    /**
     * Initializes the string to search for and the appropriate text in the Find
     * field based on the selection found in the timegraph view.
     */
    private void initFindStringFromSelection() {
        FindTarget findTarget = fFindTarget;
        if (findTarget != null && okToUse(fFindField)) {
            final ITimeGraphEntry selection = findTarget.getSelection();
            if (selection != null) {
                String fullSelection = selection.getName();
                fFindField.removeModifyListener(fFindModifyListener);
                if (fullSelection.length() > 0) {
                    fFindField.setText(fullSelection);
                }
            } else {
                if ("".equals(fFindField.getText())) { //$NON-NLS-1$
                    if (!fFindHistory.isEmpty()) {
                        fFindField.setText(fFindHistory.get(0));
                    } else {
                        fFindField.setText(""); //$NON-NLS-1$
                    }
                }
            }
            fFindField.setSelection(new Point(0, fFindField.getText().length()));
            fFindField.addModifyListener(fFindModifyListener);
        }
    }

    // ------- Options ---------------------------------------

    /**
     * Retrieves and returns the option case sensitivity from the appropriate
     * check box.
     *
     * @return <code>true</code> if case sensitive
     */
    private boolean isCaseSensitiveSearch() {
        if (okToUse(fCaseCheckBox)) {
            return fCaseCheckBox.getSelection();
        }
        return fCaseInit;
    }

    /**
     * Retrieves and returns the regEx option from the appropriate check box.
     *
     * @return <code>true</code> if case sensitive
     */
    private boolean isRegExSearch() {
        if (okToUse(fIsRegExCheckBox)) {
            return fIsRegExCheckBox.getSelection();
        }
        return fIsRegExInit;
    }

    /**
     * Retrieves and returns the option search direction from the appropriate
     * check box.
     *
     * @return <code>true</code> if searching forward
     */
    private boolean isForwardSearch() {
        if (okToUse(fForwardRadioButton)) {
            return fForwardRadioButton.getSelection();
        }
        return fForwardInit;
    }

    /**
     * Retrieves and returns the option search whole words from the appropriate
     * check box.
     *
     * @return <code>true</code> if searching for whole words
     */
    private boolean isWholeWordSetting() {
        if (okToUse(fWholeWordCheckBox)) {
            return fWholeWordCheckBox.getSelection();
        }
        return fWholeWordInit;
    }

    /**
     * Returns <code>true</code> if searching should be restricted to entire
     * words, <code>false</code> if not. This is the case if the respective
     * checkbox is turned on, regex is off, and the checkbox is enabled, i.e.
     * the current find string is an entire word.
     *
     * @return <code>true</code> if the search is restricted to whole words
     */
    private boolean isWholeWordSearch() {
        return isWholeWordSetting() && !isRegExSearch() && (okToUse(fWholeWordCheckBox) ? fWholeWordCheckBox.isEnabled() : true);
    }

    /**
     * Retrieves and returns the option wrap search from the appropriate check
     * box.
     *
     * @return <code>true</code> if wrapping while searching
     */
    private boolean isWrapSearch() {
        if (okToUse(fWrapCheckBox)) {
            return fWrapCheckBox.getSelection();
        }
        return fWrapInit;
    }

    /**
     * Creates a button.
     *
     * @param parent
     *            the parent control
     * @param label
     *            the button label
     * @param id
     *            the button id
     * @param dfltButton
     *            is this button the default button
     * @param listener
     *            a button pressed listener
     * @return the new button
     */
    private Button makeButton(Composite parent, String label, int id, boolean dfltButton, SelectionListener listener) {
        Button button = createButton(parent, id, label, dfltButton);
        button.addSelectionListener(listener);
        storeButtonWithMnemonicInMap(button);
        return button;
    }

    /**
     * Stores the button and its mnemonic in {@link #fMnemonicButtonMap}.
     *
     * @param button
     *            button whose mnemonic has to be stored
     */
    private void storeButtonWithMnemonicInMap(Button button) {
        char mnemonic = LegacyActionTools.extractMnemonic(button.getText());
        if (mnemonic != LegacyActionTools.MNEMONIC_NONE) {
            fMnemonicButtonMap.put(Character.valueOf(Character.toLowerCase(mnemonic)), button);
        }
    }

    /**
     * Sets the given status message in the status line.
     *
     * @param message
     *            the message
     */
    private void statusMessage(String message) {
        fStatusLabel.setText(message);
        getShell().getDisplay().beep();
    }

    /**
     * Locates the user's findString in the entries information of the time
     * graph view.
     *
     * @param forwardSearch
     *            the search direction
     */
    private void performSearch(boolean forwardSearch) {

        String findString = getFindString();
        if (findString != null && findString.length() > 0) {
            findAndSelect(findString, getSearchOptions(forwardSearch));
        }
        writeSelection();
        updateButtonState();
    }

    private SearchOptions getSearchOptions(boolean forwardSearch) {
        SearchOptions options = new SearchOptions();
        options.forwardSearch = forwardSearch;
        options.caseSensitive = isCaseSensitiveSearch();
        options.wrapSearch = isWrapSearch();
        options.wholeWord = isWholeWordSearch();
        options.regExSearch = isRegExSearch();
        return options;
    }

    // ------- UI creation ---------------------------------------

    /**
     * Attaches the given layout specification to the <code>component</code>.
     *
     * @param component
     *            the component
     * @param horizontalAlignment
     *            horizontal alignment
     * @param grabExcessHorizontalSpace
     *            grab excess horizontal space
     * @param verticalAlignment
     *            vertical alignment
     * @param grabExcessVerticalSpace
     *            grab excess vertical space
     */
    private static void setGridData(Control component, int horizontalAlignment, boolean grabExcessHorizontalSpace, int verticalAlignment, boolean grabExcessVerticalSpace) {
        GridData gd;
        if (component instanceof Button && (((Button) component).getStyle() & SWT.PUSH) != 0) {
            gd = (GridData) component.getLayoutData();
            gd.horizontalAlignment = GridData.FILL;
            gd.widthHint = 100;
        } else {
            gd = new GridData();
            component.setLayoutData(gd);
            gd.horizontalAlignment = horizontalAlignment;
            gd.grabExcessHorizontalSpace = grabExcessHorizontalSpace;
        }
        gd.verticalAlignment = verticalAlignment;
        gd.grabExcessVerticalSpace = grabExcessVerticalSpace;
    }

    /**
     * Updates the enabled state of the buttons.
     */
    private void updateButtonState() {
        if (okToUse(getShell()) && okToUse(fFindNextButton)) {

            boolean enable = fFindTarget != null && (fActiveShell == fParentShell || fActiveShell == getShell());
            String str = getFindString();
            boolean findString = str != null && str.length() > 0;

            fWholeWordCheckBox.setEnabled(isWord(str) && !isRegExSearch());

            fFindNextButton.setEnabled(enable && findString);
        }
    }

    /**
     * Tests whether each character in the given string is a letter.
     *
     * @param str
     *            the string to check
     * @return <code>true</code> if the given string is a word
     */
    private static boolean isWord(String str) {
        if (str == null || str.length() == 0) {
            return false;
        }

        for (int i = 0; i < str.length(); i++) {
            if (!Character.isJavaIdentifierPart(str.charAt(i))) {
                return false;
            }
        }
        return true;
    }

    /**
     * Updates the given combo with the given content.
     *
     * @param combo
     *            combo to be updated
     * @param content
     *            to be put into the combo
     */
    private static void updateCombo(Combo combo, List<String> content) {
        combo.removeAll();
        for (int i = 0; i < content.size(); i++) {
            combo.add(content.get(i).toString());
        }
    }

    // ------- open / reopen ---------------------------------------

    /**
     * Called after executed find action to update the history.
     */
    private void updateFindHistory() {
        if (okToUse(fFindField)) {
            fFindField.removeModifyListener(fFindModifyListener);

            updateHistory(fFindField, fFindHistory);
            fFindField.addModifyListener(fFindModifyListener);
        }
    }

    /**
     * Updates the combo with the history.
     *
     * @param combo
     *            to be updated
     * @param history
     *            to be put into the combo
     */
    private static void updateHistory(Combo combo, List<String> history) {
        String findString = combo.getText();
        int index = history.indexOf(findString);
        if (index != 0) {
            if (index != -1) {
                history.remove(index);
            }
            history.add(0, findString);
            Point selection = combo.getSelection();
            updateCombo(combo, history);
            combo.setText(findString);
            combo.setSelection(selection);
        }
    }

    /**
     * Sets the parent shell of this dialog to be the given shell.
     *
     * @param shell
     *            the new parent shell
     */
    @Override
    public void setParentShell(Shell shell) {
        if (shell != fParentShell) {

            if (fParentShell != null) {
                fParentShell.removeShellListener(fActivationListener);
            }

            fParentShell = shell;
            fParentShell.addShellListener(fActivationListener);
        }

        fActiveShell = shell;
    }

    // --------------- configuration handling --------------

    /**
     * Returns the dialog settings object used to share state of the find
     * dialog.
     *
     * @return the dialog settings to be used
     */
    private IDialogSettings getDialogSettings() {
        IDialogSettings settings = Activator.getDefault().getDialogSettings();
        fDialogSettings = settings.getSection(getClass().getName());
        if (fDialogSettings == null) {
            fDialogSettings = settings.addNewSection(getClass().getName());
        }
        return fDialogSettings;
    }

    /**
     * Initializes itself from the dialog settings with the same state as at the
     * previous invocation.
     */
    private void readConfiguration() {
        IDialogSettings s = getDialogSettings();

        fWrapInit = s.get(WRAP) == null || s.getBoolean(WRAP);
        fCaseInit = s.getBoolean(CASE_SENSITIVE);
        fWholeWordInit = s.getBoolean(WHOLE_WORD);
        fIsRegExInit = s.getBoolean(IS_REGEX_EXPRESSION);

        String[] findHistory = s.getArray(FIND_HISTORY);
        if (findHistory != null) {
            fFindHistory.clear();
            for (int i = 0; i < findHistory.length; i++) {
                fFindHistory.add(findHistory[i]);
            }
        }
    }

    /**
     * Stores its current configuration in the dialog store.
     */
    private void writeConfiguration() {
        IDialogSettings s = getDialogSettings();

        s.put(WRAP, fWrapInit);
        s.put(CASE_SENSITIVE, fCaseInit);
        s.put(WHOLE_WORD, fWholeWordInit);
        s.put(CASE_SENSITIVE, fIsRegExInit);

        String findString = getFindString();
        if (findString.length() > 0) {
            fFindHistory.add(0, findString);
        }
        writeHistory(fFindHistory, s, FIND_HISTORY);
    }

    /**
     * Writes the given history into the given dialog store.
     *
     * @param history
     *            the history
     * @param settings
     *            the dialog settings
     * @param sectionName
     *            the section name
     */
    private static void writeHistory(List<String> history, IDialogSettings settings, String sectionName) {
        int itemCount = history.size();
        Set<String> distinctItems = new HashSet<>(itemCount);
        for (int i = 0; i < itemCount; i++) {
            String item = history.get(i);
            if (distinctItems.contains(item)) {
                history.remove(i--);
                itemCount--;
            } else {
                distinctItems.add(item);
            }
        }

        while (history.size() > 8) {
            history.remove(8);
        }

        String[] names = new String[history.size()];
        history.toArray(names);
        settings.put(sectionName, names);

    }

    /**
     * Update the time graph viewer this dialog search in
     *
     * @param findTarget
     *            The find target. Can be <code>null</code>.
     * @param initFindString
     *            This value should be true if the find text field needs to be
     *            populated with selected entry in the find viewer, false
     *            otherwise
     */
    public void updateTarget(@Nullable FindTarget findTarget, boolean initFindString) {
        fFindTarget = findTarget;
        if (initFindString) {
            initFindStringFromSelection();
        }
        updateButtonState();
    }

    /**
     * Test if the shell to test is the parent shell of the dialog
     *
     * @param shell
     *            The shell to test
     * @return <code>true</code> if the shell to test is the parent shell,
     *         <code>false</code> otherwise
     */
    public boolean isDialogParentShell(Shell shell) {
        return shell == getParentShell();
    }

    /**
     * Adjust the index where to start the search
     *
     * @param previousIndex
     *            The previous index
     * @param count
     *            The number of items to search into
     * @param forwardSearch
     *            The search direction
     * @return The adjusted index
     */
    private static int adjustIndex(int previousIndex, int count, boolean forwardSearch) {
        int index = previousIndex;
        if (forwardSearch) {
            index = index >= count || index < 0 ? 0 : index;
        } else {
            index = index >= count || index < 0 ? count - 1 : index;
        }
        return index;
    }

    /**
     * Create a pattern from the string to find and with options provided
     *
     * This implementation is drawn from the jface implementation of
     * org.eclipse.jface.text.FindReplaceDocumentAdapter
     *
     * @param findString
     *            The string to find
     * @param caseSensitive
     *            Tells if the pattern will activate the case sensitive flag
     * @param wholeWord
     *            Tells if the pattern will activate the whole word flag
     * @param regExSearch
     *            Tells if the pattern will activate the regEx flag
     * @return The created pattern
     */
    private static @NonNull Pattern getPattern(String findString, SearchOptions options) {
        String toFind = findString;

        int patternFlags = 0;

        if (options.regExSearch) {
            toFind = substituteLinebreak(toFind);
        }

        if (!options.caseSensitive) {
            patternFlags |= Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE;
        }

        if (!options.regExSearch) {
            toFind = asRegPattern(toFind);
        }

        if (options.wholeWord) {
            toFind = "\\b" + toFind + "\\b"; //$NON-NLS-1$ //$NON-NLS-2$
        }
        Pattern pattern = Pattern.compile(toFind, patternFlags);
        return pattern;
    }

    /**
     * Substitutes \R in a regex find pattern with (?>\r\n?|\n)
     *
     * This implementation is drawn from the jface implementation of
     * org.eclipse.jface.text.FindReplaceDocumentAdapter
     *
     * @param findString
     *            the original find pattern
     * @return the transformed find pattern
     */
    private static String substituteLinebreak(String findString) {
        int length = findString.length();
        StringBuffer buf = new StringBuffer(length);

        int inCharGroup = 0;
        int inBraces = 0;
        boolean inQuote = false;
        for (int i = 0; i < length; i++) {
            char ch = findString.charAt(i);
            switch (ch) {
            case '[':
                buf.append(ch);
                if (!inQuote) {
                    inCharGroup++;
                }
                break;

            case ']':
                buf.append(ch);
                if (!inQuote) {
                    inCharGroup--;
                }
                break;

            case '{':
                buf.append(ch);
                if (!inQuote && inCharGroup == 0) {
                    inBraces++;
                }
                break;

            case '}':
                buf.append(ch);
                if (!inQuote && inCharGroup == 0) {
                    inBraces--;
                }
                break;

            case '\\':
                if (i + 1 < length) {
                    char ch1 = findString.charAt(i + 1);
                    if (inQuote) {
                        if (ch1 == 'E') {
                            inQuote = false;
                        }
                        buf.append(ch).append(ch1);
                        i++;

                    } else if (ch1 == 'R') {
                        if (inCharGroup > 0 || inBraces > 0) {
                            String msg = "TimeGrahViewer.illegalLinebreak"; //$NON-NLS-1$
                            throw new PatternSyntaxException(msg, findString, i);
                        }
                        buf.append("(?>\\r\\n?|\\n)"); //$NON-NLS-1$
                        i++;

                    } else {
                        if (ch1 == 'Q') {
                            inQuote = true;
                        }
                        buf.append(ch).append(ch1);
                        i++;
                    }
                } else {
                    buf.append(ch);
                }
                break;

            default:
                buf.append(ch);
                break;
            }

        }
        return buf.toString();
    }

    /**
     * Converts a non-regex string to a pattern that can be used with the regex
     * search engine.
     *
     * This implementation is drawn from the jface implementation of
     * org.eclipse.jface.text.FindReplaceDocumentAdapter
     *
     * @param string
     *            the non-regex pattern
     * @return the string converted to a regex pattern
     */
    private static String asRegPattern(String string) {
        StringBuffer out = new StringBuffer(string.length());
        boolean quoting = false;

        for (int i = 0, length = string.length(); i < length; i++) {
            char ch = string.charAt(i);
            if (ch == '\\') {
                if (quoting) {
                    out.append("\\E"); //$NON-NLS-1$
                    quoting = false;
                }
                out.append("\\\\"); //$NON-NLS-1$
                continue;
            }
            if (!quoting) {
                out.append("\\Q"); //$NON-NLS-1$
                quoting = true;
            }
            out.append(ch);
        }
        if (quoting) {
            out.append("\\E"); //$NON-NLS-1$
        }

        return out.toString();
    }

    private class SearchOptions {
        boolean forwardSearch;
        boolean caseSensitive;
        boolean wrapSearch;
        boolean wholeWord;
        boolean regExSearch;
    }
}
