/*******************************************************************************
 * 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
 *     Thorsten Schlathölter <tschlat@gmx.de> - Bug 467047
 ******************************************************************************/
package org.eclipse.nebula.widgets.nattable.search.strategy;

import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;

import org.eclipse.nebula.widgets.nattable.config.CellConfigAttributes;
import org.eclipse.nebula.widgets.nattable.config.IConfigRegistry;
import org.eclipse.nebula.widgets.nattable.coordinate.PositionCoordinate;
import org.eclipse.nebula.widgets.nattable.data.convert.IDisplayConverter;
import org.eclipse.nebula.widgets.nattable.layer.ILayer;
import org.eclipse.nebula.widgets.nattable.layer.LabelStack;
import org.eclipse.nebula.widgets.nattable.layer.cell.ILayerCell;
import org.eclipse.nebula.widgets.nattable.search.strategy.GridSearchStrategy.GridRectangle;
import org.eclipse.nebula.widgets.nattable.style.DisplayMode;

public final class CellDisplayValueSearchUtil {

    private CellDisplayValueSearchUtil() {
        // private default constructor for helper class
    }

    static List<PositionCoordinate> getCellCoordinates(
            ILayer contextLayer,
            int startingColumnPosition,
            int startingRowPosition,
            int width,
            int height) {
        List<PositionCoordinate> coordinates = new ArrayList<>(width * height);
        for (int columnPosition = 0; columnPosition < width; columnPosition++) {
            for (int rowPosition = 0; rowPosition < height; rowPosition++) {
                PositionCoordinate coordinate = new PositionCoordinate(
                        contextLayer,
                        startingColumnPosition,
                        startingRowPosition++);
                coordinates.add(coordinate);
            }
            startingColumnPosition++;
        }
        return coordinates;
    }

    static List<PositionCoordinate> getDescendingCellCoordinates(
            ILayer contextLayer,
            int startingColumnPosition,
            int startingRowPosition,
            int width,
            int height) {
        List<PositionCoordinate> coordinates = new ArrayList<>(width * height);
        for (int columnPosition = width; columnPosition >= 0 && startingColumnPosition >= 0; columnPosition--) {
            for (int rowPosition = height; rowPosition >= 0 && startingRowPosition >= 0; rowPosition--) {
                PositionCoordinate coordinate = new PositionCoordinate(
                        contextLayer,
                        startingColumnPosition,
                        startingRowPosition--);
                coordinates.add(coordinate);
            }
            startingColumnPosition--;
        }
        return coordinates;
    }

    static List<PositionCoordinate> getCellCoordinatesRowFirst(
            ILayer contextLayer,
            int startingColumnPosition,
            int startingRowPosition,
            int width,
            int height) {
        List<PositionCoordinate> coordinates = new ArrayList<>(width * height);
        for (int rowPosition = 0; rowPosition < height; rowPosition++) {
            for (int columnPosition = 0; columnPosition < width; columnPosition++) {
                PositionCoordinate coordinate = new PositionCoordinate(
                        contextLayer,
                        startingColumnPosition++,
                        startingRowPosition);
                coordinates.add(coordinate);
            }
            startingRowPosition++;
        }
        return coordinates;
    }

    static List<PositionCoordinate> getDescendingCellCoordinatesRowFirst(
            ILayer contextLayer,
            int startingColumnPosition,
            int startingRowPosition,
            int width,
            int height) {
        List<PositionCoordinate> coordinates = new ArrayList<>(width * height);
        for (int rowPosition = height; rowPosition >= 0 && startingRowPosition >= 0; rowPosition--) {
            for (int columnPosition = width; columnPosition >= 0 && startingColumnPosition >= 0; columnPosition--) {
                PositionCoordinate coordinate = new PositionCoordinate(
                        contextLayer,
                        startingColumnPosition--,
                        startingRowPosition);
                coordinates.add(coordinate);
            }
            startingRowPosition--;
        }
        return coordinates;
    }

    /**
     * Finds the first matching cell in a list of cells.
     *
     * @param layer
     * @param configRegistry
     * @param cellsToSearch
     * @param valueToMatch
     * @param comparator
     * @param caseSensitive
     * @param wholeWord
     * @param regex
     * @param includeCollapsed
     *            TODO currently ignored
     * @return
     * @throws PatternSyntaxException
     */
    static PositionCoordinate findCell(
            final ILayer layer,
            final IConfigRegistry configRegistry,
            final PositionCoordinate[] cellsToSearch,
            final Object valueToMatch,
            final Comparator<String> comparator,
            final boolean caseSensitive,
            final boolean wholeWord,
            final boolean regex,
            final boolean includeCollapsed) {
        String stringValue = caseSensitive ? valueToMatch.toString() : valueToMatch.toString().toLowerCase();
        Pattern pattern = regex ? Pattern.compile(stringValue) : null;
        for (int cellIndex = 0; cellIndex < cellsToSearch.length; cellIndex++) {
            final PositionCoordinate cellCoordinate = cellsToSearch[cellIndex];
            if (compare(
                    layer,
                    configRegistry,
                    pattern,
                    stringValue,
                    comparator,
                    caseSensitive,
                    wholeWord,
                    regex,
                    cellCoordinate.columnPosition,
                    cellCoordinate.rowPosition)) {
                return cellCoordinate;
            }
        }
        return null;
    }

    /**
     * Finds the first matching cell in a list of grid cell rectangles.
     *
     * @param layer
     * @param configRegistry
     * @param cellRectangles
     * @param valueToMatch
     * @param comparator
     * @param caseSensitive
     * @param wholeWord
     * @param regex
     * @param includeCollapsed
     *            TODO currently ignored
     * @return
     * @throws PatternSyntaxException
     */
    static PositionCoordinate findCell(
            final ILayer layer,
            final IConfigRegistry configRegistry,
            final List<GridRectangle> cellRectangles,
            final Object valueToMatch,
            final Comparator<String> comparator,
            final boolean caseSensitive,
            final boolean wholeWord,
            final boolean regex,
            final boolean columnFirst,
            final boolean includeCollapsed) {
        String stringValue = caseSensitive ? valueToMatch.toString() : valueToMatch.toString().toLowerCase();
        Pattern pattern = regex ? Pattern.compile(stringValue) : null;
        for (GridRectangle cellRectangle : cellRectangles) {
            int direction = cellRectangle.firstDim.size() > 0 || cellRectangle.secondDim.size() > 0 ? 1 : -1;
            for (int i = cellRectangle.firstDim.start; Math.abs(cellRectangle.firstDim.end - i) > 0; i += direction) {
                PositionCoordinate result = findCell(
                        layer,
                        configRegistry,
                        i,
                        cellRectangle.secondDim.start,
                        cellRectangle.secondDim.end,
                        direction,
                        pattern,
                        stringValue,
                        comparator,
                        caseSensitive,
                        wholeWord,
                        regex,
                        columnFirst,
                        includeCollapsed);
                if (result != null) {
                    return result;
                }
            }
        }
        return null;
    }

    /**
     * Finds the first matching cell in a table slice.
     *
     * @param layer
     * @param configRegistry
     * @param firstDimIndex
     * @param secondDimStart
     * @param secondDimEnd
     * @param direction
     * @param pattern
     * @param stringValue
     * @param comparator
     * @param caseSensitive
     * @param wholeWord
     * @param regex
     * @param columnFirst
     * @param includeCollapsed
     * @return
     * @throws PatternSyntaxException
     */
    private static PositionCoordinate findCell(
            ILayer layer,
            IConfigRegistry configRegistry,
            int firstDimIndex,
            int secondDimStart,
            int secondDimEnd,
            int direction,
            Pattern pattern,
            String stringValue,
            Comparator<String> comparator,
            boolean caseSensitive,
            boolean wholeWord,
            boolean regex,
            final boolean columnFirst,
            boolean includeCollapsed) {

        int columnPosition;
        int rowPosition;
        if (columnFirst) {
            columnPosition = firstDimIndex;
            rowPosition = secondDimStart;
        } else {
            columnPosition = secondDimStart;
            rowPosition = firstDimIndex;
        }

        for (int i = secondDimStart; direction * (secondDimEnd - i) > 0; i += direction) {
            ILayerCell cellByPosition = layer.getCellByPosition(columnPosition, rowPosition);

            // on backwards search we only consider the origin position for
            // spanned cells, otherwise the find order is different from the
            // forward search order
            if (!(direction < 0 &&
                    (cellByPosition.getOriginColumnPosition() != cellByPosition.getColumnPosition() ||
                            cellByPosition.getOriginRowPosition() != cellByPosition.getRowPosition()))) {
                PositionCoordinate searchAnchor = getSearchAnchor(cellByPosition, direction);

                // If we do not hit the searchAnchor with our current position
                // it means that we have hit a spanned cell somewhere else than
                // in the top left (for direction == 1) or bottom right (for
                // direction == -1). That in turn means that we have already
                // visited that cell. Thus we skip the compare and proceed to
                // the next position.
                if (searchAnchor.columnPosition == columnPosition && searchAnchor.rowPosition == rowPosition) {
                    if (compare(
                            layer,
                            configRegistry,
                            pattern,
                            stringValue,
                            comparator,
                            caseSensitive,
                            wholeWord,
                            regex,
                            columnPosition,
                            rowPosition)) {
                        return new PositionCoordinate(layer, columnPosition, rowPosition);
                    }
                }
            }

            if (columnFirst) {
                rowPosition += direction;
            } else {
                columnPosition += direction;
            }
        }
        return null;
    }

    /**
     * Get an anchor for the search of the given cell.
     *
     * @param cell
     * @param direction
     * @return
     */
    private static PositionCoordinate getSearchAnchor(ILayerCell cell, int direction) {
        return new PositionCoordinate(cell.getLayer(), cell.getOriginColumnPosition(), cell.getOriginRowPosition());
    }

    private static boolean compare(
            ILayer layer,
            IConfigRegistry configRegistry,
            Pattern pattern,
            String stringValue,
            Comparator<String> comparator,
            boolean caseSensitive,
            boolean wholeWord,
            boolean regex,
            int columnPosition,
            int rowPosition) {

        // Convert cell's data
        LabelStack labels = layer.getConfigLabelsByPosition(columnPosition, rowPosition);
        if (!labels.hasLabel(ISearchStrategy.SKIP_SEARCH_RESULT_LABEL)) {
            final IDisplayConverter displayConverter = configRegistry.getConfigAttribute(
                    CellConfigAttributes.DISPLAY_CONVERTER,
                    DisplayMode.NORMAL,
                    labels);
            Object dataValue = null;
            if (displayConverter != null) {
                ILayerCell cell = layer.getCellByPosition(columnPosition, rowPosition);
                if (cell != null) {
                    dataValue = displayConverter.canonicalToDisplayValue(cell, configRegistry, cell.getDataValue());
                }
            }

            // Compare with valueToMatch
            if (dataValue instanceof Comparable<?>) {
                String dataValueString = caseSensitive ? dataValue.toString() : dataValue.toString().toLowerCase();
                if (regex) {
                    if (pattern.matcher(dataValueString).matches()) {
                        return true;
                    }
                } else if (comparator.compare(stringValue, dataValueString) == 0) {
                    return true;
                } else if (!wholeWord && dataValueString.contains(stringValue)) {
                    return true;
                } else if (wholeWord) {
                    // we also need to check single words in a multi word value
                    String[] split = dataValueString.split("\\b"); //$NON-NLS-1$
                    for (String word : split) {
                        if (comparator.compare(stringValue, word) == 0) {
                            return true;
                        }
                    }
                }
            }
        }
        return false;
    }
}
