/*******************************************************************************
 * Copyright (c) 2012, 2020 Original authors and others.
 *
 * This program and the accompanying materials are made
 * available under the terms of the Eclipse Public License 2.0
 * which is available at https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *     Original authors and others - initial API and implementation
 *     Dirk Fauth <dirk.fauth@googlemail.com> - Bug 453914
 ******************************************************************************/
package org.eclipse.nebula.widgets.nattable.search.gui;

import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.regex.PatternSyntaxException;

import org.eclipse.jface.dialogs.Dialog;
import org.eclipse.jface.dialogs.IDialogConstants;
import org.eclipse.jface.dialogs.IDialogSettings;
import org.eclipse.jface.layout.GridDataFactory;
import org.eclipse.jface.layout.PixelConverter;
import org.eclipse.jface.resource.JFaceColors;
import org.eclipse.jface.resource.JFaceResources;
import org.eclipse.nebula.widgets.nattable.Messages;
import org.eclipse.nebula.widgets.nattable.NatTable;
import org.eclipse.nebula.widgets.nattable.command.ILayerCommand;
import org.eclipse.nebula.widgets.nattable.coordinate.PositionCoordinate;
import org.eclipse.nebula.widgets.nattable.layer.CompositeLayer;
import org.eclipse.nebula.widgets.nattable.layer.ILayer;
import org.eclipse.nebula.widgets.nattable.layer.ILayerListener;
import org.eclipse.nebula.widgets.nattable.layer.cell.CellDisplayConversionUtils;
import org.eclipse.nebula.widgets.nattable.layer.cell.ILayerCell;
import org.eclipse.nebula.widgets.nattable.layer.event.ILayerEvent;
import org.eclipse.nebula.widgets.nattable.search.SearchDirection;
import org.eclipse.nebula.widgets.nattable.search.action.SearchAction;
import org.eclipse.nebula.widgets.nattable.search.command.SearchCommand;
import org.eclipse.nebula.widgets.nattable.search.event.SearchEvent;
import org.eclipse.nebula.widgets.nattable.search.strategy.GridSearchStrategy;
import org.eclipse.nebula.widgets.nattable.search.strategy.ISearchStrategy;
import org.eclipse.nebula.widgets.nattable.search.strategy.SelectionSearchStrategy;
import org.eclipse.nebula.widgets.nattable.selection.SelectionLayer;
import org.eclipse.nebula.widgets.nattable.selection.command.ClearAllSelectionsCommand;
import org.eclipse.nebula.widgets.nattable.selection.command.SelectCellCommand;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.BusyIndicator;
import org.eclipse.swt.events.ModifyListener;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.layout.RowLayout;
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.Group;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Shell;

/**
 * Find Dialog. Borrows from
 * <code>org.eclipse.ui.texteditor.FindReplaceDialog</code>. Used internally by
 * {@link SearchAction}.
 */
public class SearchDialog extends Dialog {

    private class SelectionItem {
        String text;
        PositionCoordinate pos;

        SelectionItem(String text, PositionCoordinate pos) {
            this.text = text;
            this.pos = new PositionCoordinate(pos);
        }
    }

    private NatTable natTable;
    private SelectionLayer selectionLayer;
    private Comparator<?> comparator;

    /**
     * A stack for recording selections. In incremental mode, the stack is used
     * when the search term is shortened. In non-incremental mode, the stack
     * contains only the most recent selection.
     */
    private ArrayDeque<SelectionItem> selections = new ArrayDeque<>();

    // Dialog settings
    private IDialogSettings originalSettings;
    private IDialogSettings dialogSettings;
    private IDialogSettings dialogBounds;
    private Rectangle dialogPositionValue;

    // Find Combo box
    private Combo findCombo;
    private List<String> findHistory = new ArrayList<>(5);

    // Direction radio
    private Button forwardButton;
    private boolean forwardValue = true;

    // Scope radio
    private Button allButton;
    private boolean allValue = true;
    private Button selectionButton;

    // Options and cached values.
    private Button caseSensitiveButton;
    private boolean caseSensitiveValue;
    private Button wrapSearchButton;
    private boolean wrapSearchValue = true;
    private Button wholeWordButton;
    private boolean wholeWordValue;
    private Button incrementalButton;
    private boolean incrementalValue;
    private Button columnFirstButton;
    private boolean columnFirstValue;
    // TODO
    // private Button includeCollapsedButton;
    // private boolean includeCollapsedValue = true;
    private Button regexButton;
    private boolean regexValue;

    // Status label
    private Label statusLabel;

    // Find button
    private Button findButton;
    private ModifyListener findComboModifyListener;

    public SearchDialog(Shell shell, Comparator<?> comparator, int style) {
        super(shell);
        this.comparator = comparator;
        setShellStyle(getShellStyle() ^ style | SWT.MODELESS);
        setBlockOnOpen(false);
    }

    public void setInput(NatTable natTable, IDialogSettings settings) {
        if (natTable != null && natTable.equals(this.natTable)) {
            return;
        }
        this.natTable = natTable;
        if (natTable != null) {
            ILayer result = findSelectionLayer(this.natTable.getLayer());
            if (result instanceof SelectionLayer) {
                this.selectionLayer = (SelectionLayer) result;
                if (this.findButton != null && !this.findButton.isDisposed()) {
                    this.findButton.setEnabled(true);
                }
            }
        } else {
            this.selectionLayer = null;
            if (this.findButton != null && !this.findButton.isDisposed()) {
                this.findButton.setEnabled(false);
            }
        }

        this.originalSettings = settings;
        if (settings == null) {
            this.dialogSettings = null;
            this.dialogBounds = null;
        } else {
            this.dialogSettings = settings.getSection(getClass().getName());
            if (this.dialogSettings == null) {
                this.dialogSettings = settings.addNewSection(getClass().getName());
            }
            String boundsName = getClass().getName() + "_dialogBounds"; //$NON-NLS-1$
            this.dialogBounds = settings.getSection(boundsName);
            if (this.dialogBounds == null) {
                this.dialogBounds = settings.addNewSection(boundsName);
            }
        }
        readConfiguration();
    }

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

    /**
     * @since 1.5
     */
    public boolean isModal() {
        return (getShellStyle() & SWT.APPLICATION_MODAL) != 0;
    }

    /**
     * @since 1.5
     */
    public NatTable getNatTable() {
        return this.natTable;
    }

    /**
     * @since 1.5
     */
    public IDialogSettings getOriginalDialogSettings() {
        return this.originalSettings;
    }

    @Override
    public void create() {

        super.create();
        getShell().setText(Messages.getString("Search.find")); //$NON-NLS-1$
        // set dialog position
        if (this.dialogPositionValue != null) {
            getShell().setBounds(this.dialogPositionValue);
        }

        this.findCombo.removeModifyListener(this.findComboModifyListener);
        updateCombo(this.findCombo, this.findHistory);
        this.findCombo.addModifyListener(this.findComboModifyListener);

        // search SelectionLayer in layer stack
        ILayer result = findSelectionLayer(this.natTable.getLayer());
        if (result instanceof SelectionLayer) {
            this.selectionLayer = (SelectionLayer) result;
        }

        // Pick the user's selection, if possible
        PositionCoordinate pos = getPosition();
        final String text = getTextForSelection(pos);
        this.selections.push(new SelectionItem(text, pos));
        this.findCombo.setText(text);
    }

    private ILayer findSelectionLayer(ILayer layer) {
        if (layer == null || layer instanceof SelectionLayer) {
            return layer;
        } else if (layer instanceof CompositeLayer) {
            // if the layer is a CompositeLayer, search for the SelectionLayer
            // in every region as the SelectionLayer is typically placed in the
            // bottom/right most region (e.g. the body in a grid, the search is
            // performed backwards
            CompositeLayer composite = (CompositeLayer) layer;
            for (int x = composite.getLayoutXCount(); x >= 0; x--) {
                for (int y = composite.getLayoutYCount(); y >= 0; y--) {
                    ILayer childStack = composite.getChildLayerByLayoutCoordinate(x, y);
                    ILayer result = findSelectionLayer(childStack);
                    if (result instanceof SelectionLayer) {
                        return result;
                    }
                }
            }
            return null;
        } else {
            return findSelectionLayer(layer.getUnderlyingLayerByPosition(0, 0));
        }
    }

    private String getTextForSelection(PositionCoordinate selection) {
        if (this.selectionLayer == null
                || selection == null
                || selection.columnPosition == SelectionLayer.NO_SELECTION) {
            return ""; //$NON-NLS-1$
        }
        final ILayerCell cell = this.selectionLayer.getCellByPosition(selection.columnPosition, selection.rowPosition);
        if (cell == null || cell.getDataValue() == null) {
            return ""; //$NON-NLS-1$
        } else if (this.natTable != null) {
            return CellDisplayConversionUtils.convertDataType(cell, this.natTable.getConfigRegistry());
        }
        return cell.getDataValue().toString();
    }

    @Override
    public boolean close() {
        storeSettings();
        return super.close();
    }

    /**
     * Stores the current state in the dialog settings.
     */
    private void storeSettings() {
        if (getShell() == null || getShell().isDisposed()) {
            return;
        }
        this.dialogPositionValue = getShell().getBounds();
        this.forwardValue = this.forwardButton.getSelection();
        this.allValue = this.allButton.getSelection();
        this.caseSensitiveValue = this.caseSensitiveButton.getSelection();
        this.wrapSearchValue = this.wrapSearchButton.getSelection();
        this.wholeWordValue = this.wholeWordButton.getSelection();
        this.incrementalValue = this.incrementalButton.getSelection();
        this.regexValue = this.regexButton.getSelection();
        // TODO
        // includeCollapsedValue = includeCollapsedButton.getSelection();
        this.columnFirstValue = this.columnFirstButton.getSelection();
        writeConfiguration();
    }

    @Override
    protected Control createContents(final Composite parent) {
        final Composite composite = new Composite(parent, SWT.NONE);
        composite.setLayout(new GridLayout(1, false));
        GridDataFactory.fillDefaults().grab(true, true).applyTo(composite);
        GridDataFactory.fillDefaults().align(SWT.FILL, SWT.FILL)
                .grab(true, false).applyTo(createInputPanel(composite));
        GridDataFactory.fillDefaults().align(SWT.FILL, SWT.FILL)
                .grab(true, false).applyTo(createOptionsPanel(composite));
        GridDataFactory.fillDefaults().align(SWT.FILL, SWT.FILL)
                .grab(true, true).applyTo(createStatusPanel(composite));
        GridDataFactory.swtDefaults().align(SWT.FILL, SWT.BOTTOM)
                .grab(true, true).applyTo(createButtonSection(composite));
        return composite;
    }

    private Composite createStatusPanel(Composite composite) {
        Composite panel = new Composite(composite, SWT.NONE);
        panel.setLayout(new GridLayout(1, false));
        this.statusLabel = new Label(panel, SWT.LEFT);
        GridDataFactory.fillDefaults().align(SWT.FILL, SWT.CENTER)
                .grab(true, false).applyTo(this.statusLabel);
        return panel;
    }

    private Composite createButtonSection(Composite composite) {

        Composite panel = new Composite(composite, SWT.NONE);
        GridLayout layout = new GridLayout(1, false);
        panel.setLayout(layout);

        Label label = new Label(panel, SWT.LEFT);
        GridDataFactory.fillDefaults().align(SWT.FILL, SWT.CENTER)
                .grab(true, false).applyTo(label);

        this.findButton = createButton(
                panel,
                IDialogConstants.CLIENT_ID,
                Messages.getString("Search.findButtonLabel"), false); //$NON-NLS-1$
        int buttonWidth = getButtonWidthHint(this.findButton);
        GridDataFactory.swtDefaults().align(SWT.RIGHT, SWT.BOTTOM)
                .grab(false, false).hint(buttonWidth, SWT.DEFAULT)
                .applyTo(this.findButton);

        this.findButton.setEnabled(false);
        getShell().setDefaultButton(this.findButton);

        this.findButton.addSelectionListener(new SelectionAdapter() {
            @Override
            public void widgetSelected(SelectionEvent e) {
                doFind();
            }
        });

        Button closeButton = createButton(panel, IDialogConstants.CANCEL_ID,
                Messages.getString("Search.closeButtonLabel"), false); //$NON-NLS-1$
        buttonWidth = getButtonWidthHint(closeButton);
        GridDataFactory.swtDefaults().align(SWT.RIGHT, SWT.BOTTOM)
                .grab(false, false).hint(buttonWidth, SWT.DEFAULT)
                .applyTo(closeButton);

        return panel;
    }

    public static int getButtonWidthHint(Button button) {
        button.setFont(JFaceResources.getDialogFont());
        PixelConverter converter = new PixelConverter(button);
        int widthHint = converter.convertHorizontalDLUsToPixels(IDialogConstants.BUTTON_WIDTH);
        return Math.max(widthHint, button.computeSize(SWT.DEFAULT, SWT.DEFAULT, true).x);
    }

    private Composite createInputPanel(final Composite composite) {
        final Composite row = new Composite(composite, SWT.NONE);
        row.setLayout(new GridLayout(2, false));

        final Label findLabel = new Label(row, SWT.NONE);
        findLabel.setText(Messages.getString("Search.findLabel") + ":"); //$NON-NLS-1$ //$NON-NLS-2$
        GridDataFactory.fillDefaults().align(SWT.LEFT, SWT.CENTER)
                .applyTo(findLabel);

        this.findCombo = new Combo(row, SWT.DROP_DOWN | SWT.BORDER);
        GridDataFactory.fillDefaults().grab(true, false).applyTo(this.findCombo);
        this.findComboModifyListener = e -> {
            if (SearchDialog.this.allValue && SearchDialog.this.incrementalButton.isEnabled()
                    && SearchDialog.this.incrementalButton.getSelection()) {
                doIncrementalFind();
            }
            SearchDialog.this.findButton.setEnabled(SearchDialog.this.findCombo.getText().length() > 0 && SearchDialog.this.selectionLayer != null);
        };
        this.findCombo.addModifyListener(this.findComboModifyListener);
        this.findCombo.addSelectionListener(new SelectionAdapter() {
            @Override
            public void widgetDefaultSelected(SelectionEvent e) {
                if (SearchDialog.this.findButton.isEnabled()) {
                    doFind();
                }
            }
        });

        return row;
    }

    private Composite createOptionsPanel(final Composite composite) {
        final Composite row = new Composite(composite, SWT.NONE);
        row.setLayout(new GridLayout(2, true));

        final Group directionGroup = new Group(row, SWT.SHADOW_ETCHED_IN);
        GridDataFactory.fillDefaults().grab(true, true).applyTo(directionGroup);
        directionGroup.setText(Messages.getString("Search.direction")); //$NON-NLS-1$
        RowLayout rowLayout = new RowLayout(SWT.VERTICAL);
        rowLayout.marginHeight = rowLayout.marginWidth = 3;
        directionGroup.setLayout(rowLayout);
        this.forwardButton = new Button(directionGroup, SWT.RADIO);
        this.forwardButton.setText(Messages.getString("Search.forwardButtonLabel")); //$NON-NLS-1$
        this.forwardButton.setSelection(this.forwardValue);
        this.forwardButton.addSelectionListener(new SelectionAdapter() {
            @Override
            public void widgetSelected(SelectionEvent e) {
                if (SearchDialog.this.incrementalButton.getSelection()) {
                    resetIncrementalSelections();
                }
            }
        });
        final Button backwardButton = new Button(directionGroup, SWT.RADIO);
        backwardButton.setText(Messages.getString("Search.backwardButtonLabel")); //$NON-NLS-1$
        backwardButton.setSelection(!this.forwardValue);

        final Group scopeGroup = new Group(row, SWT.SHADOW_ETCHED_IN);
        GridDataFactory.fillDefaults().grab(true, true).applyTo(scopeGroup);
        scopeGroup.setText(Messages.getString("Search.scope")); //$NON-NLS-1$
        rowLayout = new RowLayout(SWT.VERTICAL);
        rowLayout.marginHeight = rowLayout.marginWidth = 3;
        scopeGroup.setLayout(rowLayout);
        this.allButton = new Button(scopeGroup, SWT.RADIO);
        this.allButton.setText(Messages.getString("Search.allLabel")); //$NON-NLS-1$
        this.allButton.setSelection(this.allValue);
        this.allButton.addSelectionListener(new SelectionAdapter() {
            @Override
            public void widgetSelected(SelectionEvent e) {
                updateButtons();
            }
        });
        this.selectionButton = new Button(scopeGroup, SWT.RADIO);
        this.selectionButton.setText(Messages.getString("Search.selectionLabel")); //$NON-NLS-1$
        this.selectionButton.setSelection(!this.allValue);

        final Group optionsGroup = new Group(row, SWT.SHADOW_ETCHED_IN);
        GridDataFactory.fillDefaults().grab(true, true).span(2, 1).applyTo(optionsGroup);
        optionsGroup.setText(Messages.getString("Search.options")); //$NON-NLS-1$
        optionsGroup.setLayout(new GridLayout(2, true));
        this.caseSensitiveButton = new Button(optionsGroup, SWT.CHECK);
        this.caseSensitiveButton.setText(Messages.getString("Search.caseSensitiveButtonLabel")); //$NON-NLS-1$
        this.caseSensitiveButton.setSelection(this.caseSensitiveValue);
        this.wrapSearchButton = new Button(optionsGroup, SWT.CHECK);
        this.wrapSearchButton.setText(Messages.getString("Search.wrapSearchButtonLabel")); //$NON-NLS-1$
        this.wrapSearchButton.setSelection(this.wrapSearchValue);
        this.wholeWordButton = new Button(optionsGroup, SWT.CHECK);
        this.wholeWordButton.setText(Messages.getString("Search.wholeWordButtonLabel")); //$NON-NLS-1$
        this.wholeWordButton.setSelection(this.wholeWordValue);
        this.wholeWordButton.setEnabled(!this.regexValue);
        this.incrementalButton = new Button(optionsGroup, SWT.CHECK);
        this.incrementalButton.setText(Messages.getString("Search.incrementalButtonLabel")); //$NON-NLS-1$
        this.incrementalButton.setSelection(this.incrementalValue);
        this.incrementalButton.setEnabled(!this.regexValue);
        this.incrementalButton.addSelectionListener(new SelectionAdapter() {
            @Override
            public void widgetSelected(SelectionEvent e) {
                if (SearchDialog.this.incrementalButton.getSelection()) {
                    resetIncrementalSelections();
                }
            }
        });
        this.regexButton = new Button(optionsGroup, SWT.CHECK);
        this.regexButton.setText(Messages.getString("Search.regexButtonLabel")); //$NON-NLS-1$
        this.regexButton.setSelection(this.regexValue);
        this.regexButton.addSelectionListener(new SelectionAdapter() {
            @Override
            public void widgetSelected(SelectionEvent e) {
                updateButtons();
            }
        });
        this.columnFirstButton = new Button(optionsGroup, SWT.CHECK);
        this.columnFirstButton.setText(Messages.getString("Search.columnFirstLabel")); //$NON-NLS-1$
        this.columnFirstButton.setSelection(this.columnFirstValue);
        // TODO
        // includeCollapsedButton = new Button(optionsGroup, SWT.CHECK);
        // includeCollapsedButton.setText(Messages.getString("Search.includeCollapsedLabel"));
        // //$NON-NLS-1$
        // includeCollapsedButton.setSelection(includeCollapsedValue);

        return row;
    }

    private void doFind() {
        doFindInit();
        doFind0(false, this.findCombo.getText());
    }

    protected void doIncrementalFind() {
        doFindInit();
        final String text = this.findCombo.getText();
        String lastText = this.selections.peek().text;
        if (lastText.startsWith(text)) {
            while (this.selections.size() > 1
                    && this.selections.peek().text.length() > text.length()) {
                this.selections.pop();
            }
            doSelect(this.selections.peek());
        } else {
            int pos;
            if (text.startsWith(lastText)) {
                pos = lastText.length();
            } else {
                pos = 0;
                resetIncrementalSelections();
            }
            // Incremental search is performed with a loop to properly
            // handle a paste, as if each character of the paste were typed
            // separately, unless the search is whole word.
            if (this.wholeWordValue) {
                doFind0(true, text);
            } else {
                for (int i = pos, n = text.length(); i < n; ++i) {
                    doFind0(true, text.substring(0, i + 1));
                }
            }
        }
    }

    private void doFindInit() {
        this.statusLabel.setText(""); //$NON-NLS-1$
        this.statusLabel.setForeground(null);
        if (this.selectionLayer != null) {
            // If the current selection is different, the user must have
            // clicked a new selection, and we need to update to that.
            PositionCoordinate pos = getPosition();
            if (!pos.equals(this.selections.peek().pos)) {
                this.selections.clear();
                this.selections.push(new SelectionItem(getTextForSelection(pos), pos));
            }
        }
    }

    private void doSelect(SelectionItem selection) {
        ILayerCommand command;
        if (selection.pos.columnPosition != SelectionLayer.NO_SELECTION) {
            command = createSelectCellCommand(selection.pos);
        } else {
            command = new ClearAllSelectionsCommand();
        }
        final ILayerCommand finalCommand = command;
        BusyIndicator.showWhile(getShell().getDisplay(), () -> SearchDialog.this.natTable.doCommand(finalCommand));
    }

    private class SearchEventListener implements ILayerListener {
        private PositionCoordinate pos;

        @Override
        public void handleLayerEvent(ILayerEvent event) {
            if (!(event instanceof SearchEvent)) {
                return;
            }
            SearchEvent searchEvent = (SearchEvent) event;
            this.pos = searchEvent.getCellCoordinate();
        }
    }

    private void doFind0(final boolean isIncremental, final String text) {

        BusyIndicator.showWhile(getShell().getDisplay(), () -> {
            PositionCoordinate previous =
                    new PositionCoordinate(SearchDialog.this.selections.peek().pos);
            try {
                final SearchCommand searchCommand = createSearchCommand(text, isIncremental);
                final SearchEventListener searchEventListener = new SearchEventListener();
                searchCommand.setSearchEventListener(searchEventListener);
                SearchDialog.this.natTable.doCommand(searchCommand);
                if (searchEventListener.pos == null) {
                    // Beep and show status if not found
                    SearchDialog.this.statusLabel.setText(Messages.getString("Search.textNotFound")); //$NON-NLS-1$
                    getShell().getDisplay().beep();
                } else {
                    SelectionItem selection = new SelectionItem(text, searchEventListener.pos);
                    SearchDialog.this.selections.push(selection);
                    if (!isIncremental) {
                        resetIncrementalSelections();
                    }
                    // Beep and show status if wrapped
                    if (previous != null && previous.columnPosition > -1) {
                        int columnDelta = selection.pos.columnPosition - previous.columnPosition;
                        int rowDelta = selection.pos.rowPosition - previous.rowPosition;
                        if (!SearchDialog.this.forwardValue) {
                            columnDelta = -columnDelta;
                            rowDelta = -rowDelta;
                        }
                        int primaryDelta = SearchDialog.this.columnFirstValue ? columnDelta : rowDelta;
                        int secondaryDelta = SearchDialog.this.columnFirstValue ? rowDelta : columnDelta;
                        if (primaryDelta < 0 || !isIncremental
                                && primaryDelta == 0 && secondaryDelta <= 0) {
                            SearchDialog.this.statusLabel.setText(Messages.getString("Search.wrappedSearch")); //$NON-NLS-1$
                            getShell().getDisplay().beep();
                        }
                    }
                }
                if (!isIncremental) {
                    updateFindHistory();
                }
            } catch (PatternSyntaxException e) {
                SearchDialog.this.statusLabel.setText(e.getLocalizedMessage());
                SearchDialog.this.statusLabel.setForeground(JFaceColors.getErrorText(SearchDialog.this.statusLabel.getDisplay()));
                getShell().getDisplay().beep();
            }
        });
    }

    private PositionCoordinate getPosition() {
        if (this.selectionLayer == null) {
            return new PositionCoordinate(
                    null,
                    SelectionLayer.NO_SELECTION,
                    SelectionLayer.NO_SELECTION);
        }
        // The SelectionLayer keeps its anchor even if it has no selection.
        // Seems wrong to me, so here I clear out the anchor.
        PositionCoordinate pos = new PositionCoordinate(this.selectionLayer.getSelectionAnchor());
        if (this.selectionLayer.getSelectedCellPositions().length == 0
                && pos.rowPosition != SelectionLayer.NO_SELECTION) {
            this.selectionLayer.clear(false);
            pos = new PositionCoordinate(this.selectionLayer.getSelectionAnchor());
        }
        return pos;
    }

    private void resetIncrementalSelections() {
        SelectionItem selection = this.selections.peek();
        this.selections.clear();
        this.selections.push(selection);
    }

    private SelectCellCommand createSelectCellCommand(PositionCoordinate selection) {
        SelectCellCommand selectCellCommand =
                new SelectCellCommand(
                        selection.getLayer(),
                        selection.columnPosition,
                        selection.rowPosition,
                        false,
                        false);
        selectCellCommand.setForcingEntireCellIntoViewport(true);
        return selectCellCommand;
    }

    private SearchCommand createSearchCommand(String text, boolean isIncremental) {
        this.forwardValue = this.forwardButton.getSelection();
        this.allValue = this.allButton.getSelection();
        this.caseSensitiveValue = this.caseSensitiveButton.getSelection();
        this.wrapSearchValue = this.wrapSearchButton.getSelection();
        this.wholeWordValue = this.wholeWordButton.getSelection();
        this.incrementalValue = this.incrementalButton.getSelection();
        this.regexValue = this.regexButton.getSelection();
        // TODO
        // includeCollapsedValue = includeCollapsedButton.getSelection();
        this.columnFirstValue = this.columnFirstButton.getSelection();

        SearchDirection searchDirection = this.forwardValue ? SearchDirection.SEARCH_FORWARD : SearchDirection.SEARCH_BACKWARDS;
        ISearchStrategy searchStrategy;
        if (this.allValue) {
            searchStrategy = new GridSearchStrategy(this.natTable.getConfigRegistry(), true, this.columnFirstValue);
        } else {
            searchStrategy = new SelectionSearchStrategy(this.natTable.getConfigRegistry(), this.columnFirstValue);
        }
        return new SearchCommand(
                text,
                this.natTable,
                searchStrategy,
                searchDirection,
                this.wrapSearchValue,
                this.caseSensitiveValue,
                !this.regexValue && this.wholeWordValue,
                !this.regexValue && this.allValue && isIncremental, this.regexValue,
                // TODO
                // includeCollapsedValue, comparator);
                false,
                this.comparator);
    }

    /**
     * Called after executed find action to update the history.
     */
    private void updateFindHistory() {
        this.findCombo.removeModifyListener(this.findComboModifyListener);
        updateHistory(this.findCombo, this.findHistory);
        this.findCombo.addModifyListener(this.findComboModifyListener);
    }

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

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

    private void updateButtons() {
        final boolean regex = this.regexButton.getSelection();
        final boolean allMode = this.allButton.getSelection();
        this.wholeWordButton.setEnabled(!regex);
        this.incrementalButton.setEnabled(!regex && allMode);
    }

    /**
     * Returns the dialog settings object used to share state between several
     * find/replace dialogs.
     *
     * @return the dialog settings to be used
     */
    private IDialogSettings getDialogSettings() {
        return this.dialogSettings;
    }

    /*
     * @see org.eclipse.jface.dialogs.Dialog#getDialogBoundsSettings()
     */
    @Override
    protected IDialogSettings getDialogBoundsSettings() {
        return this.dialogBounds;
    }

    /*
     * @see org.eclipse.jface.dialogs.Dialog#getDialogBoundsStrategy()
     */
    @Override
    protected int getDialogBoundsStrategy() {
        return DIALOG_PERSISTLOCATION | DIALOG_PERSISTSIZE;
    }

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

        this.wrapSearchValue = s.get("wrap") == null || s.getBoolean("wrap"); //$NON-NLS-1$ //$NON-NLS-2$
        this.caseSensitiveValue = s.getBoolean("casesensitive"); //$NON-NLS-1$
        this.wholeWordValue = s.getBoolean("wholeword"); //$NON-NLS-1$
        this.incrementalValue = s.getBoolean("incremental"); //$NON-NLS-1$
        this.regexValue = s.getBoolean("isRegEx"); //$NON-NLS-1$
        // TODO
        // includeCollapsedValue = s.get("includeCollapsed") == null
        // //$NON-NLS-1$
        // || s.getBoolean("includeCollapsed"); //$NON-NLS-1$
        this.columnFirstValue = s.getBoolean("columnFirst"); //$NON-NLS-1$

        String[] findHistoryConfig = s.getArray("findhistory"); //$NON-NLS-1$
        if (findHistoryConfig != null) {
            this.findHistory.clear();
            for (String history : findHistoryConfig) {
                this.findHistory.add(history);
            }
        }
    }

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

        s.put("wrap", this.wrapSearchValue); //$NON-NLS-1$
        s.put("casesensitive", this.caseSensitiveValue); //$NON-NLS-1$
        s.put("wholeword", this.wholeWordValue); //$NON-NLS-1$
        s.put("incremental", this.incrementalValue); //$NON-NLS-1$
        s.put("isRegEx", this.regexValue); //$NON-NLS-1$
        // TODO
        // s.put("includeCollapsed", includeCollapsedValue); //$NON-NLS-1$
        s.put("columnFirst", this.columnFirstValue); //$NON-NLS-1$

        String findString = this.findCombo.getText();
        if (findString.length() > 0) {
            this.findHistory.add(0, findString);
        }
        writeHistory(this.findHistory, s, "findhistory"); //$NON-NLS-1$
    }

    /**
     * Writes the given history into the given dialog store.
     *
     * @param history
     *            the history
     * @param settings
     *            the dialog settings
     * @param sectionName
     *            the section name
     */
    private 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);

    }
}
