package prefuse.action.assignment;

import java.util.HashMap;
import java.util.Map;
import java.util.logging.Logger;

import prefuse.Constants;
import prefuse.data.tuple.TupleSet;
import prefuse.util.ColorLib;
import prefuse.util.ColorMap;
import prefuse.util.DataLib;
import prefuse.util.MathLib;
import prefuse.visual.VisualItem;

/**
 * <p>
 * Assignment Action that assigns color values for a group of items based upon
 * a data field. The type of color encoding used is dependent upon the
 * reported data type. Nominal (categorical) data is encoded using a different
 * hue for each unique data value. Ordinal (ordered) and Numerical
 * (quantitative) data is shown using a grayscale color ramp. In all cases,
 * the default color palette used by this Action can be replaced with a
 * client-specified palette provided to the DataColorAction constructor.
 * </p>
 * 
 * <p>
 * The color spectra for numerical data is continuous by default, but can also
 * be binned into a few discrete steps (see {@link #setBinCount(int)}).
 * Quantitative data can also be colored on different numerical scales. The
 * default scale is a linear scale (specified by
 * {@link Constants#LINEAR_SCALE}), but logarithmic and square root scales can
 * be used (specified by {@link Constants#LOG_SCALE} and
 * {@link Constants#SQRT_SCALE} respectively. Finally, the scale can be broken
 * into quantiles, reflecting the statistical distribution of the values rather
 * than just the total data value range, using the
 * {@link Constants#QUANTILE_SCALE} value. For the quantile scale to work, you
 * also need to specify the number of bins to use (see
 * {@link #setBinCount(int)}). This value will determine the number of
 * quantiles that the data should be divided into. 
 * </p>
 * 
 * </p>
 * 
 * @author <a href="http://jheer.org">jeffrey heer</a>
 */
public class DataColorAction extends ColorAction {

    private String m_dataField;
    private int    m_type;
    private int    m_scale = Constants.LINEAR_SCALE;
    private int    m_tempScale;
    
    private double[] m_dist;
    private int      m_bins = Constants.CONTINUOUS;
    private Map      m_omap;
    private Object[] m_olist;
    private ColorMap m_cmap = new ColorMap(null,0,1);
    private int[]    m_palette;
    
    
    /**
     * Create a new DataColorAction
     * @param group the data group to process
     * @param dataField the data field to base size assignments on
     * @param dataType the data type to use for the data field. One of
     * {@link prefuse.Constants#NOMINAL}, {@link prefuse.Constants#ORDINAL},
     * or {@link prefuse.Constants#NUMERICAL}, for whether the data field
     * represents categories, an ordered sequence, or numerical values.
     * @param colorField the color field to assign
     */
    public DataColorAction(String group, String dataField, 
                             int dataType, String colorField)
    {
        super(group, colorField);
        setDataType(dataType);
        setDataField(dataField);
    }
    
    /**
     * Create a new DataColorAction
     * @param group the data group to process
     * @param dataField the data field to base size assignments on
     * @param dataType the data type to use for the data field. One of
     * {@link prefuse.Constants#NOMINAL}, {@link prefuse.Constants#ORDINAL},
     * or {@link prefuse.Constants#NUMERICAL}, for whether the data field
     * represents categories, an ordered sequence, or numerical values.
     * @param colorField the color field to assign
     * @param palette the color palette to use. See
     * {@link prefuse.util.ColorLib} for color palette generators.
     */
    public DataColorAction(String group, String dataField, 
            int dataType, String colorField, int[] palette)
    {
        super(group, colorField);
        setDataType(dataType);
        setDataField(dataField);
        m_palette = palette;
    }
    
    // ------------------------------------------------------------------------

    /**
     * Returns the data field used to encode size values.
     * @return the data field that is mapped to size values
     */
    public String getDataField() {
        return m_dataField;
    }
    
    /**
     * Set the data field used to encode size values.
     * @param field the data field to map to size values
     */
    public void setDataField(String field) {
        m_dataField = field;
    }
    
    /**
     * Return the data type used by this action. This value is one of
     * {@link prefuse.Constants#NOMINAL}, {@link prefuse.Constants#ORDINAL},
     * or {@link prefuse.Constants#NUMERICAL}.
     * @return the data type used by this action
     */
    public int getDataType() {
        return m_type;
    }
    
    /**
     * Set the data type used by this action.
     * @param type the data type used by this action, one of
     * {@link prefuse.Constants#NOMINAL}, {@link prefuse.Constants#ORDINAL},
     * or {@link prefuse.Constants#NUMERICAL}.
     */
    public void setDataType(int type) {
        if ( type < 0 || type >= Constants.DATATYPE_COUNT )
            throw new IllegalArgumentException(
                    "Unrecognized data type: "+type);
        m_type = type;
    }
    
    /**
     * Returns the scale type used for encoding color values from the data.
     * This value is only used for {@link prefuse.Constants#NUMERICAL}
     * data.
     * @return the scale type. One of
     * {@link prefuse.Constants#LINEAR_SCALE},
     * {@link prefuse.Constants#LOG_SCALE}, 
     * {@link prefuse.Constants#SQRT_SCALE},
     * {@link prefuse.Constants#QUANTILE_SCALE}.
     */
    public int getScale() {
        return m_scale;
    }
    
    /**
     * Set the scale (linear, square root, or log) to use for encoding color
     * values from the data. This value is only used for
     * {@link prefuse.Constants#NUMERICAL} data.
     * @param scale the scale type to use. This value should be one of
     * {@link prefuse.Constants#LINEAR_SCALE}, 
     * {@link prefuse.Constants#SQRT_SCALE},
     * {@link prefuse.Constants#LOG_SCALE},
     * {@link prefuse.Constants#QUANTILE_SCALE}.
     * If {@link prefuse.Constants#QUANTILE_SCALE} is used, the number of
     * bins to use must also be specified to a value greater than zero using
     * the {@link #setBinCount(int)} method.
     */
    public void setScale(int scale) {
        if ( scale < 0 || scale >= Constants.SCALE_COUNT )
            throw new IllegalArgumentException(
                    "Unrecognized scale value: "+scale);
        m_scale = scale;
    }
    
    /**
     * Returns the number of "bins" or discrete steps of color. This value
     * is only used for numerical data.
     * @return the number of bins.
     */
    public int getBinCount() {
        return m_bins;
    }

    /**
     * Sets the number of "bins" or or discrete steps of color. This value
     * is only used for numerical data.
     * @param count the number of bins to set. The value 
     * {@link Constants#CONTINUOUS} indicates not to use any binning. If the
     * scale type set using the {@link #setScale(int)} method is
     * {@link Constants#QUANTILE_SCALE}, the bin count <strong>must</strong>
     * be greater than zero.
     */
    public void setBinCount(int count) {
        if ( m_scale == Constants.QUANTILE_SCALE && count <= 0 ) {
            throw new IllegalArgumentException(
                "The quantile scale can not be used without binning. " +
                "Use a bin value greater than zero.");
        }
        m_bins = count;
    }
    
    /**
     * This operation is not supported by the DataColorAction type.
     * Calling this method will result in a thrown exception.
     * @see prefuse.action.assignment.ColorAction#setDefaultColor(int)
     * @throws UnsupportedOperationException
     */
    public void setDefaultColor(int color) {
        throw new UnsupportedOperationException();
    }
    
    /**
     * Manually sets the ordered list of values to use for color assignment.
     * Normally, this ordering is computed using the methods of the
     * {@link prefuse.util.DataLib} class. This method allows you to set your
     * own custom ordering. This ordering corresponds to the ordering of
     * colors in this action's color palette. If the provided array of values
     * is missing a value contained within the data, an exception will result
     * during execution of this action.
     * @param values the ordered list of values. If this array is missing a
     * value contained within data processed by this action, an exception
     * will be thrown when this action is run.
     */
    public void setOrdinalMap(Object[] values) {
        m_olist = values;
        m_omap = new HashMap();
        for ( int i=0; i<values.length; ++i ) {
            m_omap.put(values[i], new Integer(i));
        }
    }
    
    // ------------------------------------------------------------------------    
    
    /**
     * Set up the state of this encoding Action.
     * @see prefuse.action.EncoderAction#setup()
     */
    protected void setup() {
        int size = 64;
        
        int[] palette = m_palette;
        
        // switch up scale if necessary
        m_tempScale = m_scale;
        if ( m_scale == Constants.QUANTILE_SCALE && m_bins <= 0 ) {
            Logger.getLogger(getClass().getName()).warning(
                    "Can't use quantile scale with no binning. " +
                    "Defaulting to linear scale. Set the bin value " +
                    "greater than zero to use a quantile scale.");
            m_scale = Constants.LINEAR_SCALE;
        }
        
        // compute distribution and color map
        switch ( m_type ) {
        case Constants.NOMINAL:
        case Constants.ORDINAL:
            m_dist = getDistribution();
            size = m_omap.size();
            palette = (m_palette!=null ? m_palette : createPalette(size));
            m_cmap.setColorPalette(palette);
            m_cmap.setMinValue(m_dist[0]);
            m_cmap.setMaxValue(m_dist[1]);
            return;
        case Constants.NUMERICAL:
            m_dist = getDistribution();
            size = m_bins > 0 ? m_bins : size;
            palette = (m_palette!=null ? m_palette : createPalette(size));
            m_cmap.setColorPalette(palette);
            m_cmap.setMinValue(0.0);
            m_cmap.setMaxValue(1.0);
            return;
        }
    }
    
    protected void finish() {
        // reset scale in case it needed to be changed due to errors
        m_scale = m_tempScale;
    }
    
    /**
     * Computes the distribution (either min/max or quantile values) used to
     * help assign colors to data values.
     */
    protected double[] getDistribution() {
        TupleSet ts = m_vis.getGroup(m_group);

        if ( m_type == Constants.NUMERICAL ) {
            m_omap = null;
            if ( m_scale == Constants.QUANTILE_SCALE && m_bins > 0 ) {
                double[] values =
                        DataLib.toDoubleArray(ts.tuples(), m_dataField);
                return MathLib.quantiles(m_bins, values);
            } else {
                double[] dist = new double[2];
                dist[0] = DataLib.min(ts, m_dataField).getDouble(m_dataField);
                dist[1] = DataLib.max(ts, m_dataField).getDouble(m_dataField);
                return dist;
            }
        } else {
            if ( m_olist == null ) 
                m_omap = DataLib.ordinalMap(ts, m_dataField);
            return new double[] { 0, m_omap.size()-1 };
        }
    }
    
    /**
     * Create a color palette of the requested type and size.
     */
    protected int[] createPalette(int size) {
        switch ( m_type ) {
        case Constants.NOMINAL:
            return ColorLib.getCategoryPalette(size);
        case Constants.NUMERICAL:
        case Constants.ORDINAL:
        default:
            return ColorLib.getGrayscalePalette(size);
        }
    }
    
    /**
     * @see prefuse.action.assignment.ColorAction#getColor(prefuse.visual.VisualItem)
     */
    public int getColor(VisualItem item) {
        // check for any cascaded rules first
        Object o = lookup(item);
        if ( o != null ) {
            if ( o instanceof ColorAction ) {
                return ((ColorAction)o).getColor(item);
            } else if ( o instanceof Integer ) {
                return ((Integer)o).intValue();
            } else {
                Logger.getLogger(this.getClass().getName())
                    .warning("Unrecognized Object from predicate chain.");
            }
        }
        
        // otherwise perform data-driven assignment
        switch ( m_type ) {
        case Constants.NUMERICAL:
            double v = item.getDouble(m_dataField);
            double f = MathLib.interp(m_scale, v, m_dist);
            return m_cmap.getColor(f);
        default:
            Integer idx = (Integer)m_omap.get(item.get(m_dataField));
            return m_cmap.getColor(idx.doubleValue());
        }
    }
    
} // end of class DataColorAction
