/*
 * $Id: TableSearchable.java 3194 2009-01-21 11:39:19Z kleopatra $
 *
 * Copyright 2007 Sun Microsystems, Inc., 4150 Network Circle,
 * Santa Clara, California 95054, U.S.A. All rights reserved.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 * 
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 */
package org.jdesktop.swingx.search;

import java.awt.Rectangle;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.jdesktop.swingx.JXTable;
import org.jdesktop.swingx.decorator.AbstractHighlighter;
import org.jdesktop.swingx.decorator.Highlighter;

/**
 * An Searchable implementation for use in JXTable.
 * 
 * @author Jeanette Winzenburg
 */
public class TableSearchable extends AbstractSearchable {

    /** The target JXTable. */
    protected JXTable table;

    /**
     * Instantiates a TableSearchable with the given table as target.
     * 
     * @param table the JXTable to search.
     */
    public TableSearchable(JXTable table) {
        this.table = table;
    }

    /**
     * {@inheritDoc}
     * <p>
     * 
     * This implementation loops through the cells in a row to find a match.
     */
    @Override
    protected void findMatchAndUpdateState(Pattern pattern, int startRow,
            boolean backwards) {
        SearchResult matchRow = null;
        if (backwards) {
            // CHECK: off-one end still needed?
            // Probably not - the findXX don't have side-effects any longer
            // hmmm... still needed: even without side-effects we need to
            // guarantee calling the notfound update at the very end of the
            // loop.
            for (int r = startRow; r >= -1 && matchRow == null; r--) {
                matchRow = findMatchBackwardsInRow(pattern, r);
                updateState(matchRow);
            }
        } else {
            for (int r = startRow; r <= getSize() && matchRow == null; r++) {
                matchRow = findMatchForwardInRow(pattern, r);
                updateState(matchRow);
            }
        }
        // KEEP - JW: Needed to update if loop wasn't entered!
        // the alternative is to go one off in the loop. Hmm - which is
        // preferable?
        // updateState(matchRow);

    }

    /**
     * {@inheritDoc}
     * <p>
     * 
     * Implemented to search for an extension in the cell given by row and
     * foundColumn.
     */
    @Override
    protected SearchResult findExtendedMatch(Pattern pattern, int row) {
        return findMatchAt(pattern, row, lastSearchResult.foundColumn);
    }

    /**
     * Searches forward through columns of the given row. Starts at
     * lastFoundColumn or first column if lastFoundColumn < 0. returns an
     * appropriate SearchResult if a matching cell is found in this row or null
     * if no match is found. A row index out off range results in a no-match.
     * 
     * @param pattern <code>Pattern</code> that we will try to locate
     * @param row the row to search
     * @return an appropriate <code>SearchResult</code> if a matching cell is
     *         found in this row or null if no match is found
     */
    private SearchResult findMatchForwardInRow(Pattern pattern, int row) {
        int startColumn = (lastSearchResult.foundColumn < 0) ? 0
                : lastSearchResult.foundColumn;
        if (isValidIndex(row)) {
            for (int column = startColumn; column < table.getColumnCount(); column++) {
                SearchResult result = findMatchAt(pattern, row, column);
                if (result != null)
                    return result;
            }
        }
        return null;
    }

    /**
     * Searches forward through columns of the given row. Starts at
     * lastFoundColumn or first column if lastFoundColumn < 0. returns an
     * appropriate SearchResult if a matching cell is found in this row or null
     * if no match is found. A row index out off range results in a no-match.
     * 
     * @param pattern <code>Pattern</code> that we will try to locate
     * @param row the row to search
     * @return an appropriate <code>SearchResult</code> if a matching cell is
     *         found in this row or null if no match is found
     */
    private SearchResult findMatchBackwardsInRow(Pattern pattern, int row) {
        int startColumn = (lastSearchResult.foundColumn < 0) ? table
                .getColumnCount() - 1 : lastSearchResult.foundColumn;
        if (isValidIndex(row)) {
            for (int column = startColumn; column >= 0; column--) {
                SearchResult result = findMatchAt(pattern, row, column);
                if (result != null)
                    return result;
            }
        }
        return null;
    }

    /**
     * Matches the cell content at row/col against the given Pattern. Returns an
     * appropriate SearchResult if matching or null if no matching
     * 
     * @param pattern <code>Pattern</code> that we will try to locate
     * @param row a valid row index in view coordinates
     * @param column a valid column index in view coordinates
     * @return an appropriate <code>SearchResult</code> if matching or null
     */
    protected SearchResult findMatchAt(Pattern pattern, int row, int column) {
        String text = table.getStringAt(row, column);
        if ((text != null) && (text.length() > 0)) {
            Matcher matcher = pattern.matcher(text);
            if (matcher.find()) {
                return createSearchResult(matcher, row, column);
            }
        }
        return null;
    }

    /**
     * 
     * {@inheritDoc}
     * <p>
     * 
     * Overridden to adjust the column index to -1.
     */
    @Override
    protected int adjustStartPosition(int startIndex, boolean backwards) {
        lastSearchResult.foundColumn = -1;
        return super.adjustStartPosition(startIndex, backwards);
    }

    /**
     * {@inheritDoc}
     * <p>
     * 
     * Overridden to loop through all columns in a row.
     */
    @Override
    protected int moveStartPosition(int startRow, boolean backwards) {
        if (backwards) {
            lastSearchResult.foundColumn--;
            if (lastSearchResult.foundColumn < 0) {
                startRow--;
            }
        } else {
            lastSearchResult.foundColumn++;
            if (lastSearchResult.foundColumn >= table.getColumnCount()) {
                lastSearchResult.foundColumn = -1;
                startRow++;
            }
        }
        return startRow;
    }

    /**
     * {@inheritDoc}
     * <p>
     * 
     * Overridden to check the column index of last find.
     */
    @Override
    protected boolean isEqualStartIndex(final int startIndex) {
        return super.isEqualStartIndex(startIndex)
                && isValidColumn(lastSearchResult.foundColumn);
    }

    /**
     * Checks if row is in range: 0 <= row < getRowCount().
     * 
     * @param column the column index to check in view coordinates.
     * @return true if the column is in range, false otherwise
     */
    private boolean isValidColumn(int column) {
        return column >= 0 && column < table.getColumnCount();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected int getSize() {
        return table.getRowCount();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public JXTable getTarget() {
        return table;
    }

    /**
     * Configures the match highlighter to the current match. Ensures that the
     * matched cell is visible, if there is a match.
     * 
     * PRE: markByHighlighter
     * 
     */
    protected void moveMatchByHighlighter() {
        AbstractHighlighter searchHL = getConfiguredMatchHighlighter();
        // no match
        if (!hasMatch()) {
            return;
        } else {
            ensureInsertedSearchHighlighters(searchHL);
            table.scrollCellToVisible(lastSearchResult.foundRow,
                    lastSearchResult.foundColumn);
        }
    }

    /**
     * {@inheritDoc}
     * <p>
     * 
     * Overridden to convert the column index in the table's view coordinate
     * system to model coordinate.
     * <p>
     * 
     * PENDING JW: this is only necessary because the SearchPredicate wants its
     * highlight column in model coordinates. But code comments in the
     * SearchPredicate seem to indicate that we probably want to revise that
     * (legacy?).
     */
    @Override
    protected int convertColumnIndexToModel(int viewColumn) {
        return getTarget().convertColumnIndexToModel(viewColumn);
    }

    /**
     * Moves the row selection to the matching cell and ensures its visibility,
     * if any. Does nothing if there is no match.
     * 
     */
    protected void moveMatchBySelection() {
        if (!hasMatch()) {
            return;
        }
        int row = lastSearchResult.foundRow;
        int column = lastSearchResult.foundColumn;
        table.changeSelection(row, column, false, false);
        if (!table.getAutoscrolls()) {
            // scrolling not handled by moving selection
            Rectangle cellRect = table.getCellRect(row, column, true);
            if (cellRect != null) {
                table.scrollRectToVisible(cellRect);
            }
        }
    }

    /**
     * {@inheritDoc}
     * <p>
     */
    @Override
    protected void moveMatchMarker() {
        if (markByHighlighter()) {
            moveMatchByHighlighter();
        } else { // use selection
            moveMatchBySelection();
        }
    }

    /**
     * {@inheritDoc}
     * <p>
     */
    @Override
    protected void removeHighlighter(Highlighter searchHighlighter) {
        table.removeHighlighter(searchHighlighter);
    }

    /**
     * {@inheritDoc}
     * <p>
     */
    @Override
    protected Highlighter[] getHighlighters() {
        return table.getHighlighters();
    }

    /**
     * {@inheritDoc}
     * <p>
     */
    @Override
    protected void addHighlighter(Highlighter highlighter) {
        table.addHighlighter(highlighter);
    }

}