package uk.ac.starlink.ttools.plottask;

import gnu.jel.CompilationException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import uk.ac.starlink.table.StarTable;
import uk.ac.starlink.task.BooleanParameter;
import uk.ac.starlink.task.DoubleParameter;
import uk.ac.starlink.task.Environment;
import uk.ac.starlink.task.Parameter;
import uk.ac.starlink.task.StringParameter;
import uk.ac.starlink.task.TaskException;
import uk.ac.starlink.ttools.plot.BinnedData;
import uk.ac.starlink.ttools.plot.DataBounds;
import uk.ac.starlink.ttools.plot.Histogram;
import uk.ac.starlink.ttools.plot.HistogramPlotState;
import uk.ac.starlink.ttools.plot.MapBinnedData;
import uk.ac.starlink.ttools.plot.NormalisedBinnedData;
import uk.ac.starlink.ttools.plot.PlotData;
import uk.ac.starlink.ttools.plot.PlotState;
import uk.ac.starlink.ttools.plot.PointSequence;
import uk.ac.starlink.ttools.plot.Range;
import uk.ac.starlink.ttools.plot.Rounder;
import uk.ac.starlink.ttools.plot.Style;
import uk.ac.starlink.ttools.plot.TablePlot;

/**
 * PlotStateFactory for a histogram plot.
 *
 * @author   Mark Taylor
 * @since    15 Aug 2008
 */
public class HistogramPlotStateFactory extends PlotStateFactory {

    private final DoubleParameter yloParam_;
    private final DoubleParameter yhiParam_;
    private final BooleanParameter ylogParam_;
    private final StringParameter ylabelParam_;
    private final DoubleParameter binwidthParam_;
    private final BooleanParameter normParam_;
    private final BooleanParameter cumulativeParam_;
    private final DoubleParameter binbaseParam_;

    private static final int DEFAULT_BINS = 20;
    private static final double PAD_RATIO = 0.01;

    /**
     * Constructor.
     */
    public HistogramPlotStateFactory() {
        super( new String[] { "X", }, false, false, 0 );


        yloParam_ = new DoubleParameter( "ylo" );
        yloParam_.setPrompt( "Lower bound for Y axis" );
        yloParam_.setDescription( new String[] {
            "<p>Lower bound for Y axis.",
            "</p>",
        } );
        yloParam_.setDoubleDefault( 0. );

        yhiParam_ = new DoubleParameter( "yhi" );
        yhiParam_.setNullPermitted( true );
        yhiParam_.setPrompt( "Upper bound for Y axis" );
        yhiParam_.setDescription( new String[] {
            "<p>Upper bound for Y axis.",
            "Autogenerated from the data if not supplied.",
            "</p>",
        } );
 
        ylogParam_ = new BooleanParameter( "ylog" );
        ylogParam_.setPrompt( "Logarithmic Y axis?" );
        ylogParam_.setDescription( new String[] {
            "<p>Whether to use a logarithmic scale for the Y axis.",
            "</p>",
        } );
        ylogParam_.setBooleanDefault( false );

        ylabelParam_ = new StringParameter( "ylabel" );
        ylabelParam_.setPrompt( "Label for vertical axis" );
        ylabelParam_.setDescription( new String[] {
            "<p>Specifies a label for annotating the vertical axis.",
            "A default value based on the type of histogram will be used",
            "if no value is supplied for this parameter.",
            "</p>",
        } );
        ylabelParam_.setStringDefault( Histogram.getYInfo( false, false )
                                                .getName() );
        ylabelParam_.setNullPermitted( true );

        binwidthParam_ = new DoubleParameter( "binwidth" );
        binwidthParam_.setPrompt( "Bin width" );
        binwidthParam_.setDescription( new String[] {
            "<p>Defines the width on the X axis of histogram bins.",
            "If the X axis is logarithmic, then this is a multiplicative",
            "value.",
            "</p>",
        } );
        binwidthParam_.setNullPermitted( true );

        normParam_ = new BooleanParameter( "norm" );
        normParam_.setPrompt( "Normalise bin sizes?" );
        normParam_.setDescription( new String[] {
            "<p>Determines whether bin counts are normalised.",
            "If true, histogram bars are scaled such that",
            "summed height of all bars over the whole dataset is equal to one.",
            "Otherwise (the default), no scaling is done.",
            "</p>",
        } );
        normParam_.setBooleanDefault( false );

        cumulativeParam_ = new BooleanParameter( "cumulative" );
        cumulativeParam_.setPrompt( "Cumulative plot?" );
        cumulativeParam_.setDescription( new String[] {
            "<p>Determines whether historams are cumulative.",
            "When false (the default), the height of each bar is determined",
            "by counting the number of points which fall into the range",
            "on the X axis that it covers.",
            "When true, the height is determined by counting all the points",
            "between negative infinity and the upper bound of the range",
            "on the X axis that it covers.",
            "</p>",
        } );
        cumulativeParam_.setBooleanDefault( false );

        binbaseParam_ = new DoubleParameter( "binbase" );
        binbaseParam_.setPrompt( "Lower bound for one histogram bin" );
        binbaseParam_.setDescription( new String[] {
            "<p>Adjusts the offset of the bins.",
            "By default zero (or one for logarithmic X axis)",
            "is a boundary between bins;",
            "other boundaries are defined by this and the bin width.",
            "If this value is adjusted, the lower bound of one of the bins",
            "will be set to this value, so all the bins move along by the",
            "corresponding distance.",
            "</p>",
        } );
        binbaseParam_.setDoubleDefault( 0. );
    }

    public Parameter<?>[] getParameters() {
        String tSuffix = TABLE_VARIABLE;
        List<Parameter<?>> paramList =
            new ArrayList<Parameter<?>>( Arrays
                                        .asList( super.getParameters() ) );
        paramList.add( yloParam_ );
        paramList.add( yhiParam_ );
        paramList.add( ylogParam_ );
        paramList.add( ylabelParam_ );
        paramList.add( createWeightParameter( tSuffix ) );
        paramList.add( binwidthParam_ );
        paramList.add( normParam_ );
        paramList.add( cumulativeParam_ );
        paramList.add( binbaseParam_ );
        return paramList.toArray( new Parameter<?>[ 0 ] );
    }

    protected PlotState createPlotState() {
        return new HistogramPlotState();
    }

    protected void configurePlotState( PlotState pstate, Environment env )
            throws TaskException {
        super.configurePlotState( pstate, env );
        HistogramPlotState state = (HistogramPlotState) pstate;

        boolean xlog = state.getLogFlags()[ 0 ];
        boolean ylog = ylogParam_.booleanValue( env );
        state.setLogFlags( new boolean[] { xlog, ylog } );
        state.setFlipFlags( new boolean[] {
            state.getFlipFlags()[ 0 ],
            false,
        } );

        final double ylo;
        if ( ylog ) {
            yloParam_.setNullPermitted( true );
            yloParam_.setStringDefault( null );
            ylo = yloParam_.doubleValue( env );
        }
        else {

            /* Really, null should not be permitted here.  But it's causing
             * trouble with static parameter settings (e.g. from HTML forms).
             * So work around it. */
            yloParam_.setNullPermitted( true );
            yloParam_.setDoubleDefault( 0 );
            double yl = yloParam_.doubleValue( env );
            ylo = Double.isNaN( yl ) ? 0 : yl;
        }
        double yhi = yhiParam_.doubleValue( env );

        state.setRanges( new double[][] {
            state.getRanges()[ 0 ],
            new double[] { ylo, yhi, },
        } );

        binwidthParam_.setMinimum( xlog ? 1.0 : 0.0, false );
        double xlo = state.getRanges()[ 0 ][ 0 ];
        double xhi = state.getRanges()[ 0 ][ 1 ];
        if ( ! Double.isNaN( xlo ) && ! Double.isNaN( xhi ) ) {
            binwidthParam_.setNullPermitted( false );
            binwidthParam_.setDoubleDefault( getDefaultBinWidth( xlo, xhi,
                                                                 xlog ) );
        }
        double bw = binwidthParam_.doubleValue( env );
        state.setBinWidth( Double.isNaN( bw ) ? 0.0 : bw );

        state.setNormalised( normParam_.booleanValue( env ) );

        ylabelParam_.setStringDefault( Histogram
                                      .getYInfo( state.getWeighted(),
                                                 state.getNormalised() )
                                      .getName() );
        state.setAxisLabels( new String[] {
            state.getAxisLabels()[ 0 ],
            ylabelParam_.stringValue( env ),
        } );

        state.setNormalised( normParam_.booleanValue( env ) );
        state.setCumulative( cumulativeParam_.booleanValue( env ) );
        binbaseParam_.setDoubleDefault( state.getLogFlags()[ 0 ] ? 1. : 0. );
        state.setBinBase( binbaseParam_.doubleValue( env ) );
    }

    protected TablePlotData createPlotData( Environment env, String tLabel,
                                            StarTable table, String[] setExprs,
                                            String[] setNames,
                                            Style[] setStyles, String labelExpr,
                                            String[] coordExprs,
                                            String[] errExprs )
            throws TaskException, CompilationException {
        coordExprs =
            new String[] { coordExprs[ 0 ],
                           createWeightParameter( tLabel ).stringValue( env ) };
        return new CartesianTablePlotData( table, setExprs, setNames, setStyles,
                                           labelExpr, coordExprs, errExprs );
    }

    protected StyleFactory createStyleFactory( String prefix ) {
        return new BarStyleFactory( prefix );
    }

    protected boolean requiresConfigureFromBounds( PlotState state ) {
        for ( int idim = 0; idim < 2; idim++ ) {
            double[] range = state.getRanges()[ idim ];
            if ( Double.isNaN( range[ 0 ] ) || Double.isNaN( range[ 1 ] ) ) {
                return true;
            }
        }
        return false;
    }

    protected void configureFromBounds( PlotState pstate, DataBounds bounds ) {
        HistogramPlotState state = (HistogramPlotState) pstate;
        boolean xlog = state.getLogFlags()[ 0 ];

        double[] stateXrange = state.getRanges()[ 0 ];
        double[] calcXrange = bounds.getRanges()[ 0 ].getFiniteBounds( xlog );

        /* Work out if we need to calculate X range. */
        boolean xloCalc = Double.isNaN( stateXrange[ 0 ] );
        boolean xhiCalc = Double.isNaN( stateXrange[ 1 ] );

        /* Determine the effective X range anyway. */
        double xlo = xloCalc ? calcXrange[ 0 ] : stateXrange[ 0 ];
        double xhi = xhiCalc ? calcXrange[ 1 ] : stateXrange[ 1 ];

        /* Calculate and set the bin width if necessary. */
        double binBase = state.getBinBase();
        if ( ! ( state.getBinWidth() > 0 ) ) {
            state.setBinWidth( getDefaultBinWidth( xlo, xhi, xlog ) );
        }
        double binWidth = state.getBinWidth();

        /* If we are claculating X bounds, do some more fiddling. */
        if ( xloCalc || xhiCalc ) {

            /* Make sure that the bounds accommodate the limits of the 
             * extreme bins. */
            MapBinnedData.BinMapper<Long> mapper =
                MapBinnedData.createBinMapper( xlog, binWidth, binBase );
            if ( xloCalc ) {
                xlo = mapper.getBounds( mapper.getKey( xlo ) )[ 0 ];
            }
            if ( xhiCalc ) {
                xhi = mapper.getBounds( mapper.getKey( xhi ) )[ 1 ];
            }

            /* And then add some additional padding. */
            if ( xlog ) {
                double pad = Math.pow( xhi  / xlo, PAD_RATIO );
                if ( xloCalc ) {
                    xlo /= pad;
                }
                if ( xhiCalc ) {
                    xhi *= pad;
                }
            }
            else {
                double pad = ( xhi - xlo ) * PAD_RATIO;
                if ( xloCalc ) {
                    xlo -= pad;
                }
                if ( xhiCalc ) {
                    xhi += pad;
                }
            }
        }

        /* Work out if we need to calculate Y range. */
        double ylo = state.getRanges()[ 1 ][ 0 ];
        double yhi = state.getRanges()[ 1 ][ 1 ];
        boolean ylog = state.getLogFlags()[ 1 ];
        boolean calcYlo = Double.isNaN( ylo );
        boolean calcYhi = Double.isNaN( yhi );
        assert ylog || ! calcYlo;
        if ( calcYlo || calcYhi ) {

            /* If so, acquire a BinMapper to accumulate bin heights. */
            int nset = state.getPlotData().getSetCount();
            boolean norm = state.getNormalised();
            boolean cumulative = state.getCumulative();
            BinnedData binData =
                MapBinnedData.createBinMapper( xlog, binWidth, binBase )
                             .createBinnedData( nset );
            if ( norm ) {
                binData = new NormalisedBinnedData( binData );
            }

            /* Populate it from the data. */
            boolean[] setFlags = new boolean[ nset ];
            PointSequence pseq = state.getPlotData().getPointSequence();
            while ( pseq.next() ) {
                double[] coords = pseq.getPoint();
                double x = coords[ 0 ];
                double w = coords[ 1 ];
                if ( x >= xlo && x <= xhi && ! Double.isNaN( w ) ) {
                    for ( int is = 0; is < nset; is++ ) {
                        setFlags[ is ] = pseq.isIncluded( is );
                    }
                    binData.submitDatum( x, w, setFlags );
                }
            }
            pseq.close();

            /* Find the highest and lowest bin counts. */
            double[] sums = new double[ nset ];
            Range range = new Range();
            for ( Iterator<BinnedData.Bin> binIt =
                      binData.getBinIterator( false );
                  binIt.hasNext(); ) {
                BinnedData.Bin bin = binIt.next();
                for ( int is = 0; is < nset; is++ ) {
                    double s = bin.getWeightedCount( is );
                    sums[ is ] += s;
                    range.submit( cumulative ? sums[ is ] : s );
                }
            }
            double[] ybounds = range.getFiniteBounds( ylog );

            /* Set Y range appropriately. */
            if ( calcYlo ) {
                ylo = ybounds[ 0 ];
            }
            if ( calcYhi ) {
                yhi = ybounds[ 1 ];
            }

            /* Add some further padding. */
            if ( ylog ) {
                if ( calcYlo ) {
                    ylo /= Math.pow( yhi / ylo, PAD_RATIO );
                }
                if ( calcYhi ) {
                    yhi *= Math.pow( yhi / ylo, PAD_RATIO );
                }
            }
            else {
                if ( calcYlo && ylo != 0 ) {
                    ylo -= ( yhi - ylo ) * PAD_RATIO;
                }
                if ( calcYhi ) {
                    yhi += ( yhi - ylo ) * PAD_RATIO;
                }
            }
        }
   
        /* Store the calculated ranges into the plot state. */
        state.setRanges( new double[][] { { xlo, xhi }, { ylo, yhi } } );
    }

    /**
     * Returns a suitable default bin width value for a histogram.
     *
     * @param   xlo  lower X bound
     * @param   xhi  upper X bound
     * @param   xlog  whether X axis is logarithmic
     * @return   suitable bin width (multiplicative for xlog=true)
     */
    private double getDefaultBinWidth( double xlo, double xhi, boolean xlog ) {
        int nbin = DEFAULT_BINS;
        return xlog
             ? Rounder.LOG.round( Math.exp( Math.log( xhi / xlo ) / nbin ) )
             : Rounder.LINEAR.round( ( xhi - xlo ) / nbin );
    }

    /**
     * Constructs a weighting parameter.
     *
     * @param   tlabel   table identifier label
     * @return   new weighting parameter
     */
    private StringParameter createWeightParameter( String tlabel )  {
        StringParameter param = new StringParameter( "weight" + tlabel );
        param.setPrompt( "Histogram weighting for table " + tlabel );
        param.setDescription( new String[] {
            "<p>Defines a weighting for each point accumulated to determine",
            "the height of plotted bars.",
            "If this parameter has a value other than 1 (the default)",
            "then instead of simply accumulating the number of points per bin",
            "to determine bar height,",
            "the bar height will be the sum over the weighting expression",
            "for the points in each bin.",
            "Note that with weighting, the figure drawn is no longer",
            "strictly speaking a histogram.",
            "</p>",
            "<p>When weighted, bars can be of negative height.",
            "An anomaly of the plot as currently implemented is that the",
            "Y axis never descends below zero, so any such bars are currently",
            "invisible.",
            "This may be amended in a future release",
            "(contact the author to lobby for such an amendment).",
            "</p>",
        } );
        param.setStringDefault( "1" );
        return param;
    }
}
