package prefuse.action.layout;

import java.awt.geom.Rectangle2D;
import java.text.NumberFormat;
import java.util.Iterator;
import java.util.logging.Logger;

import prefuse.Constants;
import prefuse.data.Schema;
import prefuse.data.query.ObjectRangeModel;
import prefuse.data.tuple.TupleSet;
import prefuse.data.util.Index;
import prefuse.util.MathLib;
import prefuse.util.PrefuseLib;
import prefuse.util.ui.ValuedRangeModel;
import prefuse.visual.VisualItem;
import prefuse.visual.VisualTable;

/**
 * Layout Action that positions axis grid lines and labels for a given
 * range model.
 * 
 * @author <a href="http://jheer.org">jeffrey heer</a>
 */
public class AxisLabelLayout extends Layout {

    public static final String FRAC = "frac";
    public static final String LABEL = "_label";
    public static final String VALUE = "_value";
    
    private AxisLayout m_layout; // pointer to matching layout, if any
    private ValuedRangeModel m_model;
    private double m_lo, m_hi, m_prevlo, m_prevhi;
    
    private NumberFormat m_nf = NumberFormat.getInstance();
    private int m_axis;
    private boolean m_asc = true;
    private int m_scale = Constants.LINEAR_SCALE;
    
    private double m_spacing; // desired spacing between axis labels
    
    /**
     * Create a new AxisLabelLayout layout.
     * @param group the data group of the axis lines and labels
     * @param axis the axis type, either {@link prefuse.Constants#X_AXIS}
     * or {@link prefuse.Constants#Y_AXIS}.
     * @param values the range model that defines the span of the axis
     */
    public AxisLabelLayout(String group, int axis, ValuedRangeModel values)
    {
        this(group, axis, values, null);
    }
    
    /**
     * Create a new AxisLabelLayout layout.
     * @param group the data group of the axis lines and labels
     * @param axis the axis type, either {@link prefuse.Constants#X_AXIS}
     * or {@link prefuse.Constants#Y_AXIS}.
     * @param values the range model that defines the span of the axis
     * @param bounds the layout bounds within which to place the axis marks
     */
    public AxisLabelLayout(String group, int axis, ValuedRangeModel values,
            Rectangle2D bounds)
    {
        super(group);
        if ( bounds != null )
            setLayoutBounds(bounds);
        m_model = values;
        m_axis = axis;
        m_spacing = 50;
    }
    
    /**
     * Create a new AxisLabelLayout layout.
     * @param group the data group of the axis lines and labels
     * @param layout an {@link AxisLayout} instance to model this layout after.
     * The axis type and range model of the provided instance will be used.
     */
    public AxisLabelLayout(String group, AxisLayout layout) {
        this(group, layout, null, 50);
    }
    
    /**
     * Create a new AxisLabelLayout layout.
     * @param group the data group of the axis lines and labels
     * @param layout an {@link AxisLayout} instance to model this layout after.
     * The axis type and range model of the provided instance will be used.
     * @param bounds the layout bounds within which to place the axis marks
     */
    public AxisLabelLayout(String group, AxisLayout layout, Rectangle2D bounds) {
        this(group, layout, bounds, 50);
    }

    /**
     * Create a new AxisLabelLayout layout.
     * @param group the data group of the axis lines and labels
     * @param layout an {@link AxisLayout} instance to model this layout after.
     * The axis type and range model of the provided instance will be used.
     * @param bounds the layout bounds within which to place the axis marks
     * @param spacing the minimum spacing between axis labels
     */
    public AxisLabelLayout(String group, AxisLayout layout, Rectangle2D bounds,
            double spacing)
    {
        super(group);
        if ( bounds != null )
            setLayoutBounds(bounds);
        m_layout = layout;
        m_model = layout.getRangeModel();
        m_axis = layout.getAxis();
        m_scale = layout.getScale();
        m_spacing = spacing;
    }
    
    // ------------------------------------------------------------------------
    
    /**
     * Get the formatter used to format labels for numerical values.
     * @return the <code>NumberFormat</code> used to format numerical labels.
     */
    public NumberFormat getNumberFormat() {
        return m_nf;
    }

    /**
     * Set the formatter used to format labels for numerical values.
     * @param nf the <code>NumberFormat</code> used to format numerical labels.
     */
    public void setNumberFormat(NumberFormat nf) {
        m_nf = nf;
    }
    
    /**
     * Get the required minimum spacing between axis labels.
     * @return the axis label spacing
     */
    public double getSpacing() {
        return m_spacing;
    }

    /**
     * Set the required minimum spacing between axis labels.
     * @param spacing the axis label spacing to use
     */
    public void setSpacing(double spacing) {
        m_spacing = spacing;
    }
    
    /**
     * Returns the scale type used for the axis. This setting only applies
     * for numerical data types (i.e., when axis values are from a
     * <code>NumberValuedRange</code>).
     * @return the scale type. One of
     * {@link prefuse.Constants#LINEAR_SCALE}, 
     * {@link prefuse.Constants#SQRT_SCALE}, or
     * {@link Constants#LOG_SCALE}.
     */
    public int getScale() {
        return m_scale;
    }
    
    /**
     * Sets the scale type used for the axis. This setting only applies
     * for numerical data types (i.e., when axis values are from a
     * <code>NumberValuedRange</code>).
     * @param scale the scale type. One of
     * {@link prefuse.Constants#LINEAR_SCALE}, 
     * {@link prefuse.Constants#SQRT_SCALE}, or
     * {@link Constants#LOG_SCALE}.
     */
    public void setScale(int scale) {
        if ( scale < 0 || scale >= Constants.SCALE_COUNT ) {
            throw new IllegalArgumentException(
                "Unrecognized scale type: "+scale);
        }
        m_scale = scale;
    }
    
    /**
     * Indicates if the axis values should be presented in ascending order
     * along the axis.
     * @return true if data values increase as pixel coordinates increase,
     * false if data values decrease as pixel coordinates increase.
     */
    public boolean isAscending() {
        return m_asc;
    }
    
    /**
     * Sets if the axis values should be presented in ascending order
     * along the axis.
     * @param asc true if data values should increase as pixel coordinates
     * increase, false if data values should decrease as pixel coordinates
     * increase.
     */
    public void setAscending(boolean asc) {
        m_asc = asc;
    }
    
    /**
     * Sets the range model used to layout this axis.
     * @param model the range model
     */
    public void setRangeModel(ValuedRangeModel model) {
        m_model = model;
    }
    
    // ------------------------------------------------------------------------
    
    /**
     * @see prefuse.action.GroupAction#run(double)
     */
    public void run(double frac) {
        if ( m_model == null && m_layout != null )
            m_model = m_layout.getRangeModel();
        
        if ( m_model == null ) {
            Logger.getLogger(this.getClass().getName())
                .warning("Axis labels missing a range model.");
            return;
        }
        
        VisualTable labels = getTable();
        
        // check the axis label group to see if we can get a
        // more precise reading of the previous scale
        Double dfrac = (Double)labels.getClientProperty(FRAC);
        double fr = dfrac==null ? 1.0 : dfrac.doubleValue();
        m_prevlo = m_prevlo + fr*(m_lo-m_prevlo);
        m_prevhi = m_prevhi + fr*(m_hi-m_prevhi);
        
        // now compute the layout
        if ( m_model instanceof ObjectRangeModel )
        {   // ordinal layout
            // get the current high and low values
            m_lo = m_model.getValue();
            m_hi = m_lo + m_model.getExtent();
            
            // compute the layout
            ordinalLayout(labels);
        }
        else
        {   // numerical layout   
            // get the current high and low values
            m_lo = ((Number)m_model.getLowValue()).doubleValue();
            m_hi = ((Number)m_model.getHighValue()).doubleValue();
            
            // compute the layout
            switch ( m_scale ) {
            case Constants.LOG_SCALE:
                logLayout(labels);
                break;
            case Constants.SQRT_SCALE:
                sqrtLayout(labels);
                break;
            case Constants.LINEAR_SCALE:
            default:
                linearLayout(labels);
            }
        }
        
        // get rid of any labels that are no longer being used
        garbageCollect(labels);
    }
    
    // ------------------------------------------------------------------------
    // Quantitative Axis Layout

    /**
     * Calculates a quantitative, linearly scaled layout.
     */
    protected void linearLayout(VisualTable labels) {
        Rectangle2D b = getLayoutBounds();
        double breadth = getBreadth(b);
        
        double span = m_hi-m_lo;
        double pspan = m_prevhi-m_prevlo;
        double vlo = 0;
        if ( m_lo >= 0 ) {
            vlo = Math.pow(10, Math.floor(MathLib.log10(m_lo)));
        } else {
            vlo = -Math.pow(10, 1+Math.floor(MathLib.log10(-m_lo)));
        }
        //if ( vlo == 10 || vlo == 1 || vlo == 0.1 ) vlo = 0;
        
        // mark previously visible labels
        Iterator iter = labels.tuples();
        while ( iter.hasNext() ) {
            VisualItem item = (VisualItem)iter.next();
            reset(item);
            double v = item.getDouble(VALUE);
            double x = span==0 ? 0 : ((v-m_lo)/span)*breadth;
            set(item, x, b);
        }

        Index index = labels.index(VALUE);
        double step = getLinearStep(span, span==0 ? 0 : breadth/span);
        if ( step == 0 ) step = 1;
        int r;

        for ( double x, v=vlo; v<=m_hi; v+=step ) {
            x = ((v-m_lo)/span)*breadth;
            if ( x < -0.5 ) {
                continue;
            } else if ( (r=index.get(v)) >= 0 ) {
                VisualItem item = labels.getItem(r);
                item.setVisible(true);
                item.setEndVisible(true);
            } else {
                VisualItem item = labels.addItem();
                item.set(LABEL, m_nf.format(v));
                item.setDouble(VALUE, v);
                double f = pspan==0 ? 0 : ((v-m_prevlo)/pspan);
                if ( f <= 0 || f >= 1.0 ) item.setStartVisible(true);
                set(item, f*breadth, b);
                set(item, x, b);
            }
        }
    }
    
    /**
     * Calculates a quantitative, square root scaled layout.
     */
    protected void sqrtLayout(VisualTable labels) {
        Rectangle2D b = getLayoutBounds();
        double breadth = getBreadth(b);
        
        double span = m_hi-m_lo;
        double splo = MathLib.safeSqrt(m_prevlo);
        double spspan = MathLib.safeSqrt(m_prevhi)-splo;
        double vlo = Math.pow(10, Math.floor(MathLib.safeLog10(m_lo)));
        double slo = MathLib.safeSqrt(m_lo);
        double sspan = MathLib.safeSqrt(m_hi)-slo;
        
        // mark previously visible labels
        Iterator iter = labels.tuples();
        while ( iter.hasNext() ) {
            VisualItem item = (VisualItem)iter.next();
            reset(item);
            double v = item.getDouble(VALUE);
            double x = span==0 ? 0 : ((MathLib.safeSqrt(v)-slo)/sspan)*breadth;
            set(item, x, b);
        }
        
        Index index = labels.index(VALUE);
        double step = getLinearStep(span, breadth/span);
        if ( step == 0 ) step = 1;
        int r;
        for ( double x, v=vlo; v<=m_hi; v+=step ) {
            x = ((MathLib.safeSqrt(v)-slo)/sspan)*breadth;
            if ( x < -0.5 ) {
                continue;
            } else if ( (r=index.get(v)) >= 0 ) {
                VisualItem item = labels.getItem(r);
                item.setVisible(true);
                item.setEndVisible(true);
            } else {
                VisualItem item = labels.addItem();
                item.set(LABEL, m_nf.format(v));
                item.setDouble(VALUE, v);
                double f = spspan==0 ? 0 : ((MathLib.safeSqrt(v)-splo)/spspan);
                if ( f <= 0 || f >= 1.0 ) {
                    item.setStartVisible(true);
                }
                set(item, f*breadth, b);
                set(item, x, b);
            }
        }
    }
    
    /**
     * Calculates a quantitative, logarithmically-scaled layout.
     * TODO: This method is currently not working correctly.
     */
    protected void logLayout(VisualTable labels) {
        Rectangle2D b = getLayoutBounds();
        double breadth = getBreadth(b);
        
        labels.clear();
        
        // get span in log space
        // get log of the difference
        // if [0.1,1) round to .1's 0.1-->0.1
        // if [1,10) round to 1's  1-->1
        // if [10,100) round to 10's 10-->10
        double llo = MathLib.safeLog10(m_lo);
        double lhi = MathLib.safeLog10(m_hi);
        double lspan = lhi - llo;
        
        double d = MathLib.log10(lhi-llo);
        int e = (int)Math.floor(d);
        int ilo = (int)Math.floor(llo);
        int ihi = (int)Math.ceil(lhi);
        
        double start = Math.pow(10,ilo);
        double end = Math.pow(10, ihi);
        double step = start * Math.pow(10, e);
        //System.out.println((hi-lo)+"\t"+e+"\t"+start+"\t"+end+"\t"+step);

        // TODO: catch infinity case if diff is zero
        // figure out label cases better
        for ( double val, v=start, i=0; v<=end; v+=step, ++i ) {
            val = MathLib.safeLog10(v);
            if ( i != 0 && Math.abs(val-Math.round(val)) < 0.0001 ) {
                i = 0;
                step = 10*step;
            }
            val = ((val-llo)/lspan)*breadth;
            if ( val < -0.5 ) continue;
            
            VisualItem item = labels.addItem();
            set(item, val, b);
            String label = i==0 ? m_nf.format(v) : null;
            item.set(LABEL, label);
            item.setDouble(VALUE, v);
        }
    }
    
    /**
     * Get the "breadth" of a rectangle, based on the axis type.
     */
    protected double getBreadth(Rectangle2D b) {
        switch ( m_axis ) {
        case Constants.X_AXIS:
            return b.getWidth();
        default:
            return b.getHeight();
        }
    }
    
    /**
     * Adjust a value according to the current scale type.
     */
    protected double adjust(double v) {
        switch ( m_scale ) {
        case Constants.LOG_SCALE:
            return Math.pow(10,v);
        case Constants.SQRT_SCALE:
            return v*v;
        case Constants.LINEAR_SCALE:
        default:
            return v;
        }
    }
    
    /**
     * Compute a linear step between axis marks.
     */
    protected double getLinearStep(double span, double scale) {
        double log10 = Math.log(span)/Math.log(10);
        double step = Math.pow(10, Math.floor(log10));
        
        double delta = step * scale / m_spacing;
        if (delta > 20) {
            step /= 20;
        } else if (delta > 10) {
            step /= 10;
        } else if (delta > 5) {
            step /= 5;
        } else if (delta > 4) {
            step /= 4;
        } else if (delta > 2) {
            step /= 2;
        } else if (delta < 1) {
            step *= 2;
        }
        return step;
    }
    
    // ------------------------------------------------------------------------
    // Ordinal Axis Layout
    
    /**
     * Compute an ordinal layout of axis marks.
     */
    protected void ordinalLayout(VisualTable labels) {
        ObjectRangeModel model = (ObjectRangeModel)m_model;
        double span = m_hi-m_lo;
        double pspan = m_prevhi-m_prevlo;
        
        Rectangle2D b = getLayoutBounds();
        double breadth = getBreadth(b);
        double scale = breadth/span;
        int step = getOrdinalStep(span, scale);
        if ( step <= 0 ) step = 1;
        
        // mark previously visible labels
        Iterator iter = labels.tuples();
        while ( iter.hasNext() ) {
            VisualItem item = (VisualItem)iter.next();
            reset(item);
            double v = item.getDouble(VALUE);
            double x = span==0 ? 0 : ((v-m_lo)/span)*breadth;
            set(item, x, b);
        }

        Index index = labels.index(VALUE);
        
        // handle remaining labels
        for ( int r, v=(int)m_lo; v<=m_hi; v+=step ) {
            if ( (r=index.get((double)v)) >= 0 ) {
                VisualItem item = labels.getItem(r);
                item.set(VisualItem.LABEL, model.getObject(v).toString());
                item.setVisible(true);
                item.setEndVisible(true);
            } else {
                VisualItem item = labels.addItem();
                item.set(VisualItem.LABEL, model.getObject(v).toString());
                item.setDouble(VisualItem.VALUE, v);
                double f = pspan==0 ? 0 : ((v-m_prevlo)/pspan);
                if ( f <= 0 || f >= 1.0 ) item.setStartVisible(true);
                set(item, f*breadth, b);
                set(item, (v-m_lo)*breadth/span, b);
            }
        }
    }
    
    /**
     * Compute an ordinal step between axis marks.
     */
    protected int getOrdinalStep(double span, double scale) {
        return (scale >= m_spacing ? 1 : (int)Math.ceil(m_spacing/scale));
    }
    
    // ------------------------------------------------------------------------
    // Auxiliary methods
    
    /**
     * Set the layout values for an axis label item.
     */
    protected void set(VisualItem item, double xOrY, Rectangle2D b) {
        switch ( m_axis ) {
        case Constants.X_AXIS:
            xOrY = m_asc ? xOrY + b.getMinX() : b.getMaxX() - xOrY;
            PrefuseLib.updateDouble(item, VisualItem.X,  xOrY);
            PrefuseLib.updateDouble(item, VisualItem.Y,  b.getMinY());
            PrefuseLib.updateDouble(item, VisualItem.X2, xOrY);
            PrefuseLib.updateDouble(item, VisualItem.Y2, b.getMaxY());
            break;
        case Constants.Y_AXIS:
            xOrY = m_asc ? b.getMaxY() - xOrY - 1 : xOrY + b.getMinY();
            PrefuseLib.updateDouble(item, VisualItem.X,  b.getMinX());
            PrefuseLib.updateDouble(item, VisualItem.Y,  xOrY);
            PrefuseLib.updateDouble(item, VisualItem.X2, b.getMaxX());
            PrefuseLib.updateDouble(item, VisualItem.Y2, xOrY);
        }
    }
    
    /**
     * Reset an axis label VisualItem
     */
    protected void reset(VisualItem item) {
        item.setVisible(false);
        item.setEndVisible(false);
        item.setStartStrokeColor(item.getStrokeColor());
        item.revertToDefault(VisualItem.STROKECOLOR);
        item.revertToDefault(VisualItem.ENDSTROKECOLOR);
        item.setStartTextColor(item.getTextColor());
        item.revertToDefault(VisualItem.TEXTCOLOR);
        item.revertToDefault(VisualItem.ENDTEXTCOLOR);
        item.setStartFillColor(item.getFillColor());
        item.revertToDefault(VisualItem.FILLCOLOR);
        item.revertToDefault(VisualItem.ENDFILLCOLOR);
    }
    
    /**
     * Remove axis labels no longer being used.
     */
    protected void garbageCollect(VisualTable labels) {
        Iterator iter = labels.tuples();
        while ( iter.hasNext() ) {
            VisualItem item = (VisualItem)iter.next();
            if ( !item.isStartVisible() && !item.isEndVisible() ) {
                labels.removeTuple(item);
            }
        }
    }
    
    /**
     * Create a new table for representing axis labels.
     */
    protected VisualTable getTable() {
        TupleSet ts = m_vis.getGroup(m_group);
        if ( ts == null ) {
            Schema s = PrefuseLib.getAxisLabelSchema();
            VisualTable vt = m_vis.addTable(m_group, s);
            vt.index(VALUE);
            return vt;
        } else if ( ts instanceof VisualTable ) {
            return (VisualTable)ts;
        } else {
            throw new IllegalStateException(
                "Group already exists, not being used for labels");
        }
    }
    
} // end of class AxisLabels
