package usda.weru.weps.reports;

import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.Shape;
import java.awt.geom.Area;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.text.NumberFormat;
import java.util.Iterator;
import java.util.List;
import net.sf.jasperreports.engine.JRAbstractChartCustomizer;
import net.sf.jasperreports.engine.JRChart;
import org.jfree.chart.JFreeChart;
import org.jfree.chart.axis.AxisState;
import org.jfree.chart.axis.CategoryAxis;
import org.jfree.chart.axis.CategoryLabelPosition;
import org.jfree.chart.axis.CategoryLabelPositions;
import org.jfree.chart.axis.CategoryTick;
import org.jfree.chart.entity.CategoryLabelEntity;
import org.jfree.chart.entity.EntityCollection;
import org.jfree.chart.plot.PlotRenderingInfo;
import org.jfree.data.category.CategoryDataset;
import org.jfree.chart.axis.NumberAxis;
import org.jfree.chart.plot.CategoryPlot;
import org.jfree.chart.plot.DatasetRenderingOrder;
import org.jfree.chart.renderer.category.AreaRenderer;
import org.jfree.chart.renderer.category.BarRenderer;
import org.jfree.chart.renderer.category.CategoryItemRenderer;
import org.jfree.chart.renderer.category.LineAndShapeRenderer;
import org.jfree.chart.renderer.category.StackedBarRenderer;
import org.jfree.data.xy.XYDataset;
import org.jfree.text.TextBlock;
import org.jfree.ui.RectangleAnchor;
import org.jfree.ui.RectangleEdge;

/**
 *
 * @author Joseph Levin <joelevin@weru.ksu.edu>
 */
public class QuickPlotCustomizer extends JRAbstractChartCustomizer {

    public static final String PARAMETER_TYPE = "QUICKPLOT_TYPE";
    public static final String PARAMETER_DATA_LEFT = "leftdata";
    public static final String PARAMETER_DATA_RIGHT = "rightdata";
    public static final String COLLECTION_LEFT = "QUICKPLOT_COLLECTION_DATA_LEFT";
    public static final String COLLECTION_RIGHT = "QUICKPLOT_COLLECTION_DATA_RIGHT";
    private CategoryPlot chart_plot; // The chart being created
    private NumberAxis leftYAxis; // left Y axis 
    private NumberAxis rightYAxis; // right Y axis
    private NumberAxis blank_axis; // blank axis used in special cases
    private CategoryDataset left_data; // dataset for the left
    private CategoryDataset right_data; // dataset for the right
    private CategoryAxis x_plots; // X axis

    public enum Type {

        Line("Line", "line"),
        Bar("Bar", "bar"),
        BarStacked("Bar Stacked", "barstacked"),
        Area("Area", "area"),
        Scatter("Scatter", "scatter");
        private final String title;
        private final String key;

        private Type(String title, String key) {
            this.title = title;
            this.key = key;
        }

        public String title() {
            return title;
        }

        public String key() {
            return key;
        }
    }
    /**
     * Given a set of data print out a custom graph
     * @param jfc - is the the chart/graph that is being created.
     * @param jrc - JRChart : is a JasperReports chart (which we don't override in this method)
     */
    @Override
    public void customize(JFreeChart jfc, JRChart jrc) {
        Object left = getFieldValue(PARAMETER_DATA_LEFT);
        Object right = getFieldValue(PARAMETER_DATA_RIGHT);
        leftYAxis = this.setLeftAxisLabel();
        rightYAxis = this.setRightAxisLabel();
        chart_plot = jfc.getCategoryPlot();
        
        x_plots = this.createAxis();
        blank_axis = new NumberAxis();
        
        x_plots.setCategoryLabelPositions(CategoryLabelPositions.UP_90);
        
        if(left == null && right == null) {
            return;
        }
        
        if(left instanceof XYDataset) {
            jfc.getXYPlot().setDataset((XYDataset) left);
        }
        
        if(left instanceof CategoryDataset && right instanceof CategoryDataset) {
            left_data = (CategoryDataset) left;
            right_data = (CategoryDataset) right;
            
            this.customizeBothGraphs();
            
        } else if(left instanceof CategoryDataset && right == null) {
            left_data = (CategoryDataset) left;
            
            this.customizeLeftGraph();
            
        } else if(right instanceof CategoryDataset && left == null) {
            right_data = (CategoryDataset) right;
            
            this.customizeRightGraph();
        }
    }
    /**
     * A method used when both Left and Right data is present.
     * 
     * If both data doesn't contain all zeros print it out with a Y-axis range
     * 
     * otherwise if one data set contains all zeros print out the other data
     * with its range.
     * 
     * if both data is all zeroes then print zero line.
     * 
     */
    private void customizeBothGraphs() {
        if(!dataSetIsZero(left_data) && !dataSetIsZero(right_data)) {
            this.setLeftDataRange(leftYAxis);
            this.setRightDataRange(rightYAxis);
        } else if(!dataSetIsZero(left_data) && dataSetIsZero(right_data)) {
            this.setLeftDataRange(leftYAxis);

            blank_axis = this.setBlankAxis();
            blank_axis.setRange(0, this.addMax((double) this.getParameterValue("RMax")));
            blank_axis.setTickLabelsVisible(false);
            this.setRightDataRange(blank_axis);
        } else if(dataSetIsZero(left_data) && !dataSetIsZero(right_data)) {
            this.setRightDataRange(rightYAxis);

            blank_axis.setRange(0, this.addMax((Double) this.getParameterValue("RMax")));
            blank_axis.setTickLabelsVisible(false);

            this.setLeftDataRange(blank_axis);
        } else {
            blank_axis = (NumberAxis) chart_plot.getRangeAxis();
            NumberFormat sd = NumberFormat.getNumberInstance();
            sd.setMaximumFractionDigits(2);
            blank_axis.setNumberFormatOverride(sd);
            this.setLeftDataRange(blank_axis);
            this.setRightDataRange(blank_axis);
        }
        
        if(left_data.getRowCount() < right_data.getRowCount()) {
            chart_plot.setDatasetRenderingOrder(DatasetRenderingOrder.FORWARD);
        }
        
        this.setLeftGraphColor();
 
        chart_plot.setDomainAxis(x_plots);
        chart_plot.setRenderer(1, this.renderRightGraph(chart_plot.getRenderer()));
        this.mapLeftDataSetToAxis();
        this.mapRightDataSetToAxis();
    }
    
    /**
     * Print just left data with differences for data with all zeros.
     */
    private void customizeLeftGraph() {
        if(!dataSetIsZero(left_data)) {
            this.setLeftDataRange(leftYAxis);
        } else {
            blank_axis = (NumberAxis) chart_plot.getRangeAxis();
            NumberFormat sd = NumberFormat.getNumberInstance();
            sd.setMaximumFractionDigits(2);
            blank_axis.setNumberFormatOverride(sd);
            this.setLeftDataRange(blank_axis);
        }

        this.setLeftGraphColor();
        chart_plot.setDomainAxis(x_plots);    
        this.mapLeftDataSetToAxis();
    }
    
    /**
     * Print just right data with differences for data with all zeros.
     */
    private void customizeRightGraph() {
        if(!dataSetIsZero(right_data)) {
                this.setRightDataRange(rightYAxis);
                
                blank_axis.setRange(0, this.addMax((Double) this.getParameterValue("RMax")));
                blank_axis.setTickLabelsVisible(false);
                chart_plot.setRangeAxis(0, blank_axis);
            } else {
                chart_plot.setDataset(right_data);
                blank_axis = setBlankAxis();
                this.setRightDataRange(blank_axis);
                
                for(int i = 0; i < right_data.getRowCount(); i++) {
                    chart_plot.getRenderer(0).setSeriesVisible(i, false);
                }
                chart_plot.getRangeAxis(0).setTickLabelsVisible(false);
            }

            chart_plot.setRenderer(1, this.renderRightGraph(chart_plot.getRenderer()));
            chart_plot.setDomainAxis(x_plots);            
            this.mapRightDataSetToAxis();
    }
    
    /**
     * set the label for the left axis
     * @return - a NumberAxis labeled for the left axis with a new Max value
     */
    private NumberAxis setLeftAxisLabel() {
        NumberAxis leftAxis = new NumberAxis();        
        leftAxis.setLabel("See legend for Left Axis units");    
        double LMax = (Double) this.getParameterValue("LMax");
        double newLMax = addMax(LMax);
        leftAxis.setRange((Double) this.getParameterValue("LMin"), newLMax);
        
        return leftAxis;
    }
    
    /**
     * set the label for the right axis
     * @return - a NumberAxis labeled for the right axis with a new Max value
     */
    private NumberAxis setRightAxisLabel() {
        NumberAxis rightAxis = new NumberAxis();        
        rightAxis.setLabel("See legend for Right Axis units");
        double RMax = (Double) this.getParameterValue("RMax");
        double newRMax = addMax(RMax);
        rightAxis.setRange((Double) this.getParameterValue("RMin"), newRMax);
        
        return rightAxis;
    }
    
    /**
     * Set a blank axis for data with all zeroes
     * @return - return a blank axis
     */
    private NumberAxis setBlankAxis() {
        NumberAxis axis = new NumberAxis();
        axis.setRange(chart_plot.getRangeAxis().getRange());
        NumberFormat sd = NumberFormat.getNumberInstance();
        sd.setMaximumFractionDigits(2);
        sd.setMinimumFractionDigits(1);
        axis.setNumberFormatOverride(sd);
        
        return axis;
    }
    
    /**
     * Given a Y-axis set the left data to that Y-axis
     * @param axis - Either a blank-axis or the Left axis
     */
    private void setLeftDataRange(NumberAxis axis) {
        chart_plot.setRangeAxis(0, axis);
        chart_plot.setDataset(0, left_data);
    }
    
    /**
     * Given a Y-axis set the right data to that Y-axis
     * @param axis - Either a blank-axis or the Right axis
     */
    private void setRightDataRange(NumberAxis axis) {
        chart_plot.setRangeAxis(1, axis);
        chart_plot.setDataset(1, right_data);
    }
    
    /**
     * Helper method to map the left data set to the graph
     */
    private void mapLeftDataSetToAxis() {
        chart_plot.mapDatasetToRangeAxis(0, 0);
        chart_plot.mapDatasetToDomainAxis(0, 0);
    }
    
    /**
     * Helper method to map the right data set to the graph
     */
    private void mapRightDataSetToAxis() {
        chart_plot.mapDatasetToRangeAxis(1, 1);
        chart_plot.mapDatasetToDomainAxis(1, 0);
    }
    
    /**
     * Helper function that adds 10 percent of the Max Value to itself in order
     * to prevent the Max value from not showing up in the graph.
     * 
     * get 10% of number then round it off.
     * 
     * @param maxVal - the maxVal from the data set
     * @return - a new max value with 10% of the max value added and rounded off.
     */
    private Double addMax(Double maxVal) {
        double tenPercentMax = maxVal / 10.0;
        double addedMax = maxVal + tenPercentMax;
        
        return addedMax;
    }
    
    /**
     * Helper method to determine if a data set only contains zero.
     * 
     * Important note: this only will succeed true if all of the data is zero
     * Since we are dealing with a left and right collection of data. If either
     * collection contains a row of data with a nonzero value it will be false.
     * And depending on the number of selections, there can be multiple rows of
     * data. So if one row is empty it will still come up as false.
     * 
     * This is fine because we only want to post the zero data on a single line.
     * 
     * @param data - data set containing either left or right axis data
     * @return - true if data is only 0 values and false otherwise
     */
    private boolean dataSetIsZero(CategoryDataset data) {
        for(int row = 0; row < data.getRowCount(); row++) {
            for(int col = 0; col < data.getColumnCount(); col++) {
                if(!data.getValue(row, col).equals(0.0)) {
                    return false;
                }
            }
        }
        
        return true;
    }

    /**
     * Creates a new renderer for the right axis graph
     *
     * @param input - the left graph renderer
     * @return - a copy of whatever subclass the left graph was, but with a different series of colors
     */
    private CategoryItemRenderer renderRightGraph(Object input) {
        CategoryItemRenderer output;
        
        if (input instanceof AreaRenderer) {
            output = new AreaRenderer();
            this.setRightGraphColor(output);

        } else if (input instanceof StackedBarRenderer) {
            output = new StackedBarRenderer();
            ((StackedBarRenderer) output).setShadowVisible(false);
            ((StackedBarRenderer) input).setShadowVisible(false);
            this.setRightGraphColor(output);   
        } else if (input instanceof BarRenderer) {
            output = new BarRenderer();
            ((BarRenderer) output).setShadowVisible(false);
            ((BarRenderer) input).setShadowVisible(false);
            this.setRightGraphColor(output);
        } else if (input instanceof LineAndShapeRenderer) {
            output = new LineAndShapeRenderer();
            
            ((LineAndShapeRenderer) output).setBaseLinesVisible(((LineAndShapeRenderer) input).getBaseLinesVisible());
            this.setRightGraphColor(output);
        } else {
            output = null;
        }
        
        return output;
    }
    
    /**
     * Helper Method used by renderRightGraph
     * 
     * set the paint color for the right graph.
     * 
     * @param output - the right graph render object
     */
    private void setRightGraphColor(CategoryItemRenderer output) {
                
            output.setSeriesPaint(0, Color.decode("#0083FF"));  // Blue
            output.setSeriesPaint(1, Color.decode("#E800D3"));  // Pink
            output.setSeriesPaint(2, Color.decode("#2F9B02"));  // Green    
            output.setSeriesPaint(3, Color.decode("#1500E5"));  // Dark Blue
            output.setSeriesPaint(4, Color.decode("#184E01"));  // Darker green
            output.setSeriesPaint(5, Color.decode("#C87533"));
            output.setSeriesPaint(6, Color.decode("#8E388E"));
            output.setSeriesPaint(7, Color.decode("#C5B358"));
            output.setSeriesPaint(8, Color.decode("#8B4789"));
            output.setSeriesPaint(9, Color.decode("#473C8B"));
            output.setSeriesPaint(10, Color.decode("#698B22"));
            output.setSeriesPaint(11, Color.decode("#E3CF57"));
            output.setSeriesPaint(12, Color.decode("#33A1C9"));
            output.setSeriesPaint(13, Color.decode("#8B7B8B"));
            output.setSeriesPaint(14, Color.decode("#E3A869"));
            output.setSeriesPaint(15, Color.decode("#292421"));
    }
    
    /**
     * Method to set the color for the left graph.
     * 
     * if the there are only one set of data for left and right then we can need
     * to set the opacity of our left graph so that we can see the right graph
     * 
     * @param cp - the 
     */
    private void setLeftGraphColor() {
        if(left_data.getRowCount() == 1 && right_data.getRowCount() == 1
                && chart_plot.getRenderer(0) instanceof BarRenderer) {
            chart_plot.getRenderer(0).setSeriesPaint(0, new Color(255, 0, 0, 125));
        } else {
            chart_plot.getRenderer().setSeriesPaint(0, Color.decode("#FF0000")); // bright red 
        }
        
        chart_plot.getRenderer().setSeriesPaint(1, Color.BLACK);
        chart_plot.getRenderer().setSeriesPaint(2, Color.decode("#008080")); // blue green
        chart_plot.getRenderer().setSeriesPaint(3, Color.decode("#850000")); // dark red
        chart_plot.getRenderer().setSeriesPaint(4, Color.decode("#878787")); // medium grey
        chart_plot.getRenderer().setSeriesPaint(5, Color.decode("#0E2F44"));
        chart_plot.getRenderer().setSeriesPaint(6, Color.decode("#3399FF"));
        chart_plot.getRenderer().setSeriesPaint(7, Color.decode("#990000"));
        chart_plot.getRenderer().setSeriesPaint(8, Color.decode("#40E0D0"));
        chart_plot.getRenderer().setSeriesPaint(9, Color.decode("#468499"));
        chart_plot.getRenderer().setSeriesPaint(10, Color.decode("#CD7F32"));
        chart_plot.getRenderer().setSeriesPaint(11, Color.decode("#C0C0C0"));
        chart_plot.getRenderer().setSeriesPaint(12, Color.decode("#CFB53B"));
        chart_plot.getRenderer().setSeriesPaint(13, Color.decode("#663399"));
        chart_plot.getRenderer().setSeriesPaint(14, Color.decode("#228B22"));
        chart_plot.getRenderer().setSeriesPaint(15, Color.decode("#4169E1"));
    }
    
    public CategoryAxis createAxis() {
        CategoryAxis axis2 = new CategoryAxis() {
            private static final long serialVersionUID = 1L;

            /**
             * Draws the category labels and returns the updated axis state.
             *
             * @param g2 the graphics device (<code>null</code> not permitted).
             * @param plotArea the plot area (<code>null</code> not permitted).
             * @param dataArea the area inside the axes (<code>null</code> not
             * permitted).
             * @param edge the axis location (<code>null</code> not permitted).
             * @param state the axis state (<code>null</code> not permitted).
             * @param plotState collects information about the plot
             * (<code>null</code> permitted).
             *
             * @return The updated axis state (never <code>null</code>).
             */
            @Override
            protected AxisState drawCategoryLabels(Graphics2D g2, Rectangle2D plotArea,
                    Rectangle2D dataArea, RectangleEdge edge, AxisState state, PlotRenderingInfo plotState) {

                if (state == null) {
                    throw new IllegalArgumentException("Null 'state' argument.");
                }
                
                if (isTickLabelsVisible()) {
                    List<?> ticks = refreshTicks(g2, state, plotArea, edge);
                    state.setTicks(ticks);

                    int categoryIndex = 0;
                    Iterator<?> iterator = ticks.iterator();

                    //keep track of the last drawn label
                    Area lastDrawnLabel = null;
                    while (iterator.hasNext()) {
                        CategoryTick tick = (CategoryTick) iterator.next();
                        g2.setFont(getTickLabelFont(tick.getCategory()));
                        g2.setPaint(getTickLabelPaint(tick.getCategory()));

                        CategoryLabelPosition position = this.getCategoryLabelPositions().getLabelPosition(edge);
                        double x0 = 0.0;
                        double x1 = 0.0;
                        double y0 = 0.0;
                        double y1 = 0.0;
                        if (edge == RectangleEdge.TOP) {
                            x0 = getCategoryStart(categoryIndex, ticks.size(), dataArea, edge);
                            x1 = getCategoryEnd(categoryIndex, ticks.size(), dataArea, edge);
                            y1 = state.getCursor() - this.getCategoryLabelPositionOffset();
                            y0 = y1 - state.getMax();
                        } else if (edge == RectangleEdge.BOTTOM) {
                            x0 = getCategoryStart(categoryIndex, ticks.size(), dataArea, edge);
                            x1 = getCategoryEnd(categoryIndex, ticks.size(), dataArea, edge);
                            y0 = state.getCursor() + this.getCategoryLabelPositionOffset();
                            y1 = y0 + state.getMax();
                        } else if (edge == RectangleEdge.LEFT) {
                            y0 = getCategoryStart(categoryIndex, ticks.size(), dataArea, edge);
                            y1 = getCategoryEnd(categoryIndex, ticks.size(), dataArea, edge);
                            x1 = state.getCursor() - this.getCategoryLabelPositionOffset();
                            x0 = x1 - state.getMax();
                        } else if (edge == RectangleEdge.RIGHT) {
                            y0 = getCategoryStart(categoryIndex, ticks.size(), dataArea, edge);
                            y1 = getCategoryEnd(categoryIndex, ticks.size(), dataArea, edge);
                            x0 = state.getCursor() + this.getCategoryLabelPositionOffset();
                            x1 = x0 - state.getMax();
                        }                   

                        Rectangle2D area = new Rectangle2D.Double(x0, y0, (x1 - x0), (y1 - y0));
                        Point2D anchorPoint = RectangleAnchor.coordinates(area, position.getCategoryAnchor());
                        TextBlock block = tick.getLabel();                        
                        Shape bounds = block.calculateBounds(g2, (float) anchorPoint.getX(),
                                (float) anchorPoint.getY(), position.getLabelAnchor(),
                                (float) anchorPoint.getX(), (float) anchorPoint.getY(), position.getAngle());
                        Area labelArea = new Area(bounds);

                        if (lastDrawnLabel == null || !lastDrawnLabel.intersects(area)) {
                            block.draw(g2, (float) anchorPoint.getX(), (float) anchorPoint.getY(),
                                    position.getLabelAnchor(), (float) anchorPoint.getX(),
                                    (float) anchorPoint.getY(), position.getAngle());
                            lastDrawnLabel = labelArea;
                        }
                        if (plotState != null && plotState.getOwner() != null) {
                            EntityCollection entities = plotState.getOwner().getEntityCollection();
                            if (entities != null) {
                                String tooltip = getCategoryLabelToolTip(tick.getCategory());
                                entities.add(new CategoryLabelEntity(tick.getCategory(), bounds, tooltip, null));
                            }
                        }
                        categoryIndex++;
                    }

                    if (edge.equals(RectangleEdge.TOP)) {
                        double h = state.getMax() + this.getCategoryLabelPositionOffset();
                        state.cursorUp(h);
                    } else if (edge.equals(RectangleEdge.BOTTOM)) {
                        double h = state.getMax() + this.getCategoryLabelPositionOffset();
                        state.cursorDown(h);
                    } else if (edge == RectangleEdge.LEFT) {
                        double w = state.getMax() + this.getCategoryLabelPositionOffset();
                        state.cursorLeft(w);
                    } else if (edge == RectangleEdge.RIGHT) {
                        double w = state.getMax() + this.getCategoryLabelPositionOffset();
                        state.cursorRight(w);
                    }
                }
                return state;
            }
        };
        return axis2;
    }
}
