/*
 * $Id: HighlightPredicate.java 3935 2011-03-02 19:06:41Z kschaefe $
 *
 * Copyright 2006 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.decorator;

import java.awt.Component;
import java.awt.Insets;
import java.awt.Point;
import java.awt.Rectangle;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;

import javax.swing.AbstractButton;
import javax.swing.Icon;
import javax.swing.JComponent;
import javax.swing.JLabel;
import javax.swing.SwingConstants;
import javax.swing.SwingUtilities;

import org.jdesktop.swingx.rollover.RolloverProducer;
import org.jdesktop.swingx.util.Contract;

/**
 * A controller which decides whether or not a visual decoration should
 * be applied to the given Component in the given ComponentAdapter state. 
 * This is a on/off <b>decision</b> only, the actual decoration is
 * left to the AbstractHighlighter which typically respects this predicate. <p>
 * 
 * Note: implementations should be immutable because <code>Highlighter</code>s 
 * guarantee to notify listeners on any state change which might effect the highlight. 
 * They can't comply to that contract if predicate internal state changes under their 
 * feet. If dynamic predicate state is required, the safe alternative is to create 
 * and set a new predicate.<p>
 * 
 * 
 * @author Jeanette Winzenburg
 * 
 * @see AbstractHighlighter
 */
public interface HighlightPredicate {

    /**
     * Returns a boolean to indicate whether the component should be 
     * highlighted.<p>
     * 
     * Note: both parameters should be considered strictly read-only!
     * 
    * @param renderer the cell renderer component that is to be decorated,
    *    must not be null
    * @param adapter the ComponentAdapter for this decorate operation,
    *    most not be null
    * @return a boolean to indicate whether the component should be highlighted.
     */
    boolean isHighlighted(Component renderer, ComponentAdapter adapter);

    
//--------------------- implemented Constants    
    /**
     * Unconditional true.
     */
    public static final HighlightPredicate ALWAYS = new HighlightPredicate() {

        /**
         * {@inheritDoc} <p>
         * 
         * Implemented to return true always.
         */
        @Override
        public boolean isHighlighted(Component renderer, ComponentAdapter adapter) {
            return true;
        }
        
    };

    /**
     * Unconditional false.
     */
    public static final HighlightPredicate NEVER = new HighlightPredicate() {

        /**
         * {@inheritDoc} <p>
         * 
         * Implemented to return false always.
         */
        @Override
        public boolean isHighlighted(Component renderer, ComponentAdapter adapter) {
            return false;
        }
        
    };
    
    /**
     * Rollover  Row.
     */
    public static final HighlightPredicate ROLLOVER_ROW = new HighlightPredicate() {
        
        /**
         * @inheritDoc
         * Implemented to return true if the adapter's component is enabled and
         * the row of its rollover property equals the adapter's row, returns
         * false otherwise.
         * 
         * @see org.jdesktop.swingx.rollover.RolloverProducer
         */
        @Override
        public boolean isHighlighted(Component renderer, ComponentAdapter adapter) {
            if (!adapter.getComponent().isEnabled()) return false;
            Point p = (Point) adapter.getComponent().getClientProperty(
                    RolloverProducer.ROLLOVER_KEY);
            return p != null &&  p.y == adapter.row;
        }
        
    };
    
    /**
     * Rollover  Column.
     */
    public static final HighlightPredicate ROLLOVER_COLUMN = new HighlightPredicate() {
        
        /**
         * @inheritDoc
         * Implemented to return true if the adapter's component is enabled and
         * the column of its rollover property equals the adapter's columns, returns
         * false otherwise.
         * 
         * @see org.jdesktop.swingx.rollover.RolloverProducer
         */
        @Override
        public boolean isHighlighted(Component renderer, ComponentAdapter adapter) {
            if (!adapter.getComponent().isEnabled()) return false;
            Point p = (Point) adapter.getComponent().getClientProperty(
                    RolloverProducer.ROLLOVER_KEY);
            return p != null &&  p.x == adapter.column;
        }
        
    };
    /**
     * Rollover  Cell.
     */
    public static final HighlightPredicate ROLLOVER_CELL = new HighlightPredicate() {
        
        /**
         * @inheritDoc
         * Implemented to return true if the adapter's component is enabled and
         * the column of its rollover property equals the adapter's columns, returns
         * false otherwise.
         * 
         * @see org.jdesktop.swingx.rollover.RolloverProducer
         */
        @Override
        public boolean isHighlighted(Component renderer, ComponentAdapter adapter) {
            if (!adapter.getComponent().isEnabled()) return false;
            Point p = (Point) adapter.getComponent().getClientProperty(
                    RolloverProducer.ROLLOVER_KEY);
            return p != null  && p.y == adapter.row &&  p.x == adapter.column;
        }
        
    };
    
    /**
     * Is editable.
     */
    public static final HighlightPredicate EDITABLE = new HighlightPredicate() {
        /**
         * {@inheritDoc} <p>
         * 
         * Implemented to return true is the given adapter isEditable, false otherwise.
         */
        @Override
        public boolean isHighlighted(Component renderer, ComponentAdapter adapter) {
            return adapter.isEditable();
        }
    };
    
    /**
     * Convenience for read-only (same as !editable).
     */
    public static final HighlightPredicate READ_ONLY = new HighlightPredicate() {
        /**
         * {@inheritDoc} <p>
         * 
         * Implemented to return false is the given adapter isEditable, true otherwise.
         */
        @Override
        public boolean isHighlighted(Component renderer, ComponentAdapter adapter) {
            return !adapter.isEditable();
        }
    };
    
    /**
     * Leaf predicate.
     */
    public static final HighlightPredicate IS_LEAF = new HighlightPredicate() {
        /**
         * {@inheritDoc} <p>
         * 
         * Implemented to return true if the given adapter isLeaf, false otherwise.
         */
        @Override
        public boolean isHighlighted(Component renderer, ComponentAdapter adapter) {
            return adapter.isLeaf();
        }
    };
    
    /**
     * Folder predicate - convenience: same as !IS_LEAF.
     */
    public static final HighlightPredicate IS_FOLDER = new HighlightPredicate() {
        /**
         * {@inheritDoc} <p>
         * 
         * Implemented to return false if the given adapter isLeaf, true otherwise.
         */
        @Override
        public boolean isHighlighted(Component renderer, ComponentAdapter adapter) {
            return !adapter.isLeaf();
        }
    };
    
    /**
     * Selected predicate.
     */
    public static final HighlightPredicate IS_SELECTED = new HighlightPredicate() {

        @Override
        public boolean isHighlighted(Component renderer,
                ComponentAdapter adapter) {
            return adapter.isSelected();
        }
        
    };
    
    /**
     * Determines if the displayed text is truncated.
     *
     * @author Karl Schaefer
     */
    public static final HighlightPredicate IS_TEXT_TRUNCATED = new HighlightPredicate() {
        @Override
        public boolean isHighlighted(Component renderer, ComponentAdapter adapter) {
            JComponent c = renderer instanceof JComponent ? (JComponent) renderer : null;
            String text = adapter.getString();
            Icon icon = null;
            //defaults from JLabel
            int verticalAlignment = SwingConstants.CENTER;
            int horizontalAlignment = SwingConstants.LEADING;
            int verticalTextPosition = SwingConstants.CENTER;
            int horizontalTextPosition = SwingConstants.TRAILING;
            int gap = 0;
            
            if (renderer instanceof JLabel) {
                icon = ((JLabel) renderer).getIcon();
                gap = ((JLabel) renderer).getIconTextGap();
            } else if (renderer instanceof AbstractButton) {
                icon = ((AbstractButton) renderer).getIcon();
                gap = ((AbstractButton) renderer).getIconTextGap();
            }
            
            Rectangle cellBounds = adapter.getCellBounds();
            if (c != null && c.getBorder() != null) {
                Insets insets = c.getBorder().getBorderInsets(c);
                cellBounds.width -= insets.left + insets.right;
                cellBounds.height -= insets.top + insets.bottom;
            }
            
            String result = SwingUtilities.layoutCompoundLabel(c, renderer
                    .getFontMetrics(renderer.getFont()), text, icon, verticalAlignment,
                    horizontalAlignment, verticalTextPosition, horizontalTextPosition, cellBounds,
                    new Rectangle(), new Rectangle(), gap);
            
            return !text.equals(result);
        }
    };
    
    /**
     * Focus predicate.
     */
    public static final HighlightPredicate HAS_FOCUS = new HighlightPredicate() {
        /**
         * {@inheritDoc} <p>
         * 
         * Implemented to return truw if the given adapter hasFocus, false otherwise.
         */
        @Override
        public boolean isHighlighted(Component renderer, ComponentAdapter adapter) {
            return adapter.hasFocus();
        }
    };
    /**
     * Even rows.
     * 
     * PENDING: this is zero based (that is "really" even 0, 2, 4 ..), differing 
     * from the old AlternateRowHighlighter.
     * 
     */
    public static final HighlightPredicate EVEN = new HighlightPredicate() {

        @Override
        public boolean isHighlighted(Component renderer, ComponentAdapter adapter) {
            return adapter.row % 2 == 0;
        }
        
    };
    
    /**
     * Odd rows.
     * 
     * PENDING: this is zero based (that is 1, 3, 4 ..), differs from 
     * the old implementation which was one based?
     * 
     */
    public static final HighlightPredicate ODD = new HighlightPredicate() {

        @Override
        public boolean isHighlighted(Component renderer, ComponentAdapter adapter) {
            return !EVEN.isHighlighted(renderer, adapter);
        }
        
    };
    
    /**
     * Negative BigDecimals.
     */
     public static final HighlightPredicate BIG_DECIMAL_NEGATIVE = new HighlightPredicate() {

        @Override
        public boolean isHighlighted(Component renderer,
                ComponentAdapter adapter) {
            return (adapter.getValue() instanceof BigDecimal) 
               && ((BigDecimal) adapter.getValue()).compareTo(BigDecimal.ZERO) < 0;
        }
        
    };

    /**
     * Negative Number.
     */
     public static final HighlightPredicate INTEGER_NEGATIVE = new HighlightPredicate() {

        @Override
        public boolean isHighlighted(Component renderer,
                ComponentAdapter adapter) {
            return (adapter.getValue() instanceof Number) 
               && ((Number) adapter.getValue()).intValue() < 0;
        }
        
    };

    // PENDING: these general type empty arrays don't really belong here?
    public static final HighlightPredicate[] EMPTY_PREDICATE_ARRAY = new HighlightPredicate[0];
    public static final Object[] EMPTY_OBJECT_ARRAY = new Object[0];
    public static final Integer[] EMPTY_INTEGER_ARRAY = new Integer[0];
    
//----------------- logical implementations amongst HighlightPredicates
    
    /**
     * Negation of a HighlightPredicate.
     */
    public static class NotHighlightPredicate implements HighlightPredicate {
        
        private HighlightPredicate predicate;
        
        /**
         * Instantiates a not against the given predicate.
         * @param predicate the predicate to negate, must not be null.
         * @throws NullPointerException if the predicate is null
         */
        public NotHighlightPredicate(HighlightPredicate predicate) {
            if (predicate == null) 
                throw new NullPointerException("predicate must not be null");
            this.predicate = predicate;
        }
        
        /**
         * {@inheritDoc}
         * Implemented to return the negation of the given predicate.
         */
        @Override
        public boolean isHighlighted(Component renderer, ComponentAdapter adapter) {
            return !predicate.isHighlighted(renderer, adapter);
        }

        /**
         * @return the contained HighlightPredicate.
         */
        public HighlightPredicate getHighlightPredicate() {
            return predicate;
        }

    }
    
    /**
     * Ands a list of predicates.
     */
    public static class AndHighlightPredicate implements HighlightPredicate {
        
        private List<HighlightPredicate> predicate;
        
        /**
         * Instantiates a predicate which ands all given predicates.
         * @param predicate zero or more not null predicates to and
         * @throws NullPointerException if the predicate is null
         */
        public AndHighlightPredicate(HighlightPredicate... predicate) {
            this.predicate = Arrays.asList(Contract.asNotNull(predicate, "predicate must not be null"));
        }
        
        /**
         * Instantiates a predicate which ANDs all contained predicates.
         * @param list a collection with zero or more not null predicates to AND
         * @throws NullPointerException if the collection is null
         */
        public AndHighlightPredicate(Collection<HighlightPredicate> list) {
            this.predicate = new ArrayList<HighlightPredicate>(Contract.asNotNull(list, "predicate list must not be null"));
        }

        /**
         * {@inheritDoc}
         * Implemented to return false if any of the contained predicates is
         * false or if there are no predicates.
         */
        @Override
        public boolean isHighlighted(Component renderer, ComponentAdapter adapter) {
            for (HighlightPredicate hp : predicate) {
                if (!hp.isHighlighted(renderer, adapter)) return false;
            }
            return !predicate.isEmpty();
        }

        /**
         * @return the contained HighlightPredicates.
         */
        public HighlightPredicate[] getHighlightPredicates() {
            if (predicate.isEmpty()) return EMPTY_PREDICATE_ARRAY;
            return predicate.toArray(new HighlightPredicate[predicate.size()]);
        }
        
        
    }
    
    /**
     * Or's a list of predicates.
     */
    public static class OrHighlightPredicate implements HighlightPredicate {
        
        private List<HighlightPredicate> predicate;
        
        /**
         * Instantiates a predicate which ORs all given predicates.
         * @param predicate zero or more not null predicates to OR
         * @throws NullPointerException if the predicate is null
         */
        public OrHighlightPredicate(HighlightPredicate... predicate) {
            this.predicate = Arrays.asList(Contract.asNotNull(predicate, "predicate must not be null"));
        }
        
        /**
         * Instantiates a predicate which ORs all contained predicates.
         * @param list a collection with zero or more not null predicates to OR
         * @throws NullPointerException if the collection is null
         */
        public OrHighlightPredicate(Collection<HighlightPredicate> list) {
            this.predicate = new ArrayList<HighlightPredicate>(Contract.asNotNull(list, "predicate list must not be null"));
        }

        /**
         * {@inheritDoc}
         * Implemented to return true if any of the contained predicates is
         * true.
         */
        @Override
        public boolean isHighlighted(Component renderer, ComponentAdapter adapter) {
            for (HighlightPredicate hp : predicate) {
                if (hp.isHighlighted(renderer, adapter)) return true;
            }
            return false;
        }
        /**
         * @return all registered predicates
         */
        public HighlightPredicate[] getHighlightPredicates() {
            if (predicate.isEmpty()) return EMPTY_PREDICATE_ARRAY;
            return predicate.toArray(new HighlightPredicate[predicate.size()]);
        }
        
    }
    
//------------------------ coordinates
    
    public static class RowGroupHighlightPredicate implements HighlightPredicate {

        private int linesPerGroup;

        /**
         * Instantiates a predicate with the given grouping.
         * 
         * @param linesPerGroup number of lines constituting a group, must
         *    be > 0
         * @throws IllegalArgumentException if linesPerGroup < 1   
         */
        public RowGroupHighlightPredicate(int linesPerGroup) {
            if (linesPerGroup < 1) 
                throw new IllegalArgumentException("a group contain at least 1 row, was: " + linesPerGroup);
            this.linesPerGroup = linesPerGroup;
        }
        
        /**
         * {@inheritDoc}
         * Implemented to return true if the adapter's row falls into a 
         * odd group number.
         */
        @Override
        public boolean isHighlighted(Component renderer, ComponentAdapter adapter) {
            // JW: oddness check is okay - adapter.row must be a valid view coordinate
            return (adapter.row / linesPerGroup) % 2 == 1;
        }

        /**
         * 
         * @return the number of lines per group.
         */
        public int getLinesPerGroup() {
            return linesPerGroup;
        }
        
    }
    
    /**
     * A HighlightPredicate based on column index.
     * 
     */
    public static class ColumnHighlightPredicate implements HighlightPredicate {
        List<Integer> columnList;
        
        /**
         * Instantiates a predicate which returns true for the
         * given columns in model coordinates.
         * 
         * @param columns the columns to highlight in model coordinates.
         */
        public ColumnHighlightPredicate(int... columns) {
            columnList = new ArrayList<Integer>();
            for (int i = 0; i < columns.length; i++) {
                columnList.add(columns[i]);
            }
        }
        
        /**
         * {@inheritDoc}
         * 
         * This implementation returns true if the adapter's column
         * is contained in this predicates list.
         * 
         */
        @Override
        public boolean isHighlighted(Component renderer, ComponentAdapter adapter) {
            int modelIndex = adapter.convertColumnIndexToModel(adapter.column);
            return columnList.contains(modelIndex);
        }

        /**
         * PENDING JW: get array of int instead of Integer?
         * 
         * @return the columns indices in model coordinates to highlight
         */
        public Integer[] getColumns() {
            if (columnList.isEmpty()) return EMPTY_INTEGER_ARRAY;
            return columnList.toArray(new Integer[columnList.size()]);
        }
        
    }
    

    /**
     * A HighlightPredicate based on column identifier.
     * 
     */
    public static class IdentifierHighlightPredicate implements HighlightPredicate {
        List<Object> columnList;
        
        /**
         * Instantiates a predicate which returns true for the
         * given column identifiers.
         * 
         * @param columns the identitiers of the columns to highlight.
         */
        public IdentifierHighlightPredicate(Object... columns) {
            columnList = new ArrayList<Object>();
            for (int i = 0; i < columns.length; i++) {
                columnList.add(columns[i]);
            }
        }
        
        /**
         * {@inheritDoc}
         * 
         * This implementation returns true if the adapter's column
         * is contained in this predicates list.
         * 
         */
        @Override
        public boolean isHighlighted(Component renderer, ComponentAdapter adapter) {
            int modelIndex = adapter.convertColumnIndexToModel(adapter.column);
            Object identifier = adapter.getColumnIdentifierAt(modelIndex);
            return identifier != null ? columnList.contains(identifier) : false;
        }

        /**
         * @return the identifiers
         */
        public Object[] getIdentifiers() {
            if (columnList.isEmpty()) return EMPTY_OBJECT_ARRAY;
            return columnList.toArray(new Object[0]);
        }
        
    }
    

    
    /**
     * A {@code HighlightPredicate} based on adapter depth.
     * 
     * @author Karl Schaefer
     */
    public static class DepthHighlightPredicate implements HighlightPredicate {
        private List<Integer> depthList;
        
        /**
         * Instantiates a predicate which returns true for the
         * given depths.
         * 
         * @param depths the depths to highlight
         */
        public DepthHighlightPredicate(int... depths) {
            depthList = new ArrayList<Integer>();
            for (int i = 0; i < depths.length; i++) {
                depthList.add(depths[i]);
            }
        }
        
        /**
         * {@inheritDoc}
         * 
         * This implementation returns true if the adapter's depth is contained
         * in this predicates list.
         * 
         */
        @Override
        public boolean isHighlighted(Component renderer,
                ComponentAdapter adapter) {
            int depth = adapter.getDepth();
            return depthList.contains(depth);
        }

        /**
         * @return array of numbers representing different depths
         */
        public Integer[] getDepths() {
            if (depthList.isEmpty()) return EMPTY_INTEGER_ARRAY;
            return depthList.toArray(new Integer[depthList.size()]);
        }
        
    }
    
    //--------------------- value testing
    
    /**
     * Predicate testing the componentAdapter value against a fixed
     * Object. 
     */
    public static class EqualsHighlightPredicate implements HighlightPredicate {

        private Object compareValue;
        
        /**
         * Instantitates a predicate with null compare value.
         *
         */
        public EqualsHighlightPredicate() {
            this(null);
        }
        /**
         * Instantiates a predicate with the given compare value.
         * PENDING JW: support array? 
         * @param compareValue the fixed value to compare the 
         *   adapter against.
         */
        public EqualsHighlightPredicate(Object compareValue) {
            this.compareValue = compareValue;
        }
        
        /**
         * {@inheritDoc}
         * 
         * Implemented to return true if the adapter value equals the 
         * this predicate's compare value.
         */
        @Override
        public boolean isHighlighted(Component renderer, ComponentAdapter adapter) {
            if (compareValue == null) return adapter.getValue() == null;
            return compareValue.equals(adapter.getValue());
        }
        
        /**
         * @return the value this predicate checks against.
         */
        public Object getCompareValue() {
            return compareValue;
        }
        
    }

    /**
     * Predicate testing the componentAdapter value type against a given
     * Class. 
     */
    public static class TypeHighlightPredicate implements HighlightPredicate {

        private Class<?> clazz;
        
        /**
         * Instantiates a predicate with Object.clazz. This is essentially the
         * same as testing the adapter's value against null.
         *
         */
        public TypeHighlightPredicate() {
            this(Object.class);
        }
        
        /**
         * Instantiates a predicate with the given compare class.<p>
         * 
         * PENDING JW: support array? 
         * 
         * @param compareValue the fixed class to compare the 
         *   adapter value against, must not be null
         *   
         * @throws NullPointerException if the class is null.
         */
        public TypeHighlightPredicate(Class<?> compareValue) {
            this.clazz = Contract.asNotNull(compareValue, "compare class must not be null");
        }
        
        /**
         * {@inheritDoc}
         * 
         * Implemented to return true if the adapter value is an instance
         * of this predicate's class type.
         */
        @Override
        public boolean isHighlighted(Component renderer, ComponentAdapter adapter) {
            return adapter.getValue() != null ? 
                    clazz.isAssignableFrom(adapter.getValue().getClass()) : false;
        }
        
        /**
         * @return type of predicate compare class
         */
        public Class<?> getType() {
            return clazz;
        }
        
    }

    /**
     * Predicate testing the componentAdapter column type against a given
     * Class. 
     */
    public static class ColumnTypeHighlightPredicate implements HighlightPredicate {

        private Class<?> clazz;
        
        /**
         * Instantitates a predicate with Object.class. <p>
         * 
         * PENDING JW: this constructor is not very useful ... concrete implementations of 
         * ComponentAdapter are required  to return a not-null from their 
         * getColumnClass() methods). 
         *
         */
        public ColumnTypeHighlightPredicate() {
            this(Object.class);
        }
        
        /**
         * Instantitates a predicate with the given compare class.
         * 
         * @param compareValue the fixed class to compare the 
         *   adapter's column class against, must not be null
         *   
         * @throws NullPointerException if the class is null.
         *   
         */
        public ColumnTypeHighlightPredicate(Class<?> compareValue) {
            this.clazz = Contract.asNotNull(compareValue, "compare class must not be null");
        }
        
        /**
         * @inheritDoc
         * 
         * Implemented to return true if the adapter value is an instance
         * of this predicate's class type.
         */
        @Override
        public boolean isHighlighted(Component renderer, ComponentAdapter adapter) {
            return clazz.isAssignableFrom(adapter.getColumnClass());
        }
        
        public Class<?> getType() {
            return clazz;
        }
        
    }
}
