/* ===========================================================
 * Orson Charts : a 3D chart library for the Java(tm) platform
 * ===========================================================
 * 
 * (C)opyright 2013-2016, by Object Refinery Limited.  All rights reserved.
 * 
 * http://www.object-refinery.com/orsoncharts/index.html
 * 
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 * 
 * [Oracle and Java are registered trademarks of Oracle and/or its affiliates. 
 * Other names may be trademarks of their respective owners.]
 * 
 * If you do not wish to be bound by the terms of the GPL, an alternative
 * commercial license can be purchased.  For details, please see visit the
 * Orson Charts home page:
 * 
 * http://www.object-refinery.com/orsoncharts/index.html
 * 
 */

package com.orsoncharts.plot;

import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Paint;
import java.awt.Stroke;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;

import com.orsoncharts.Chart3D;
import com.orsoncharts.ChartElementVisitor;
import com.orsoncharts.axis.Axis3DChangeEvent;
import com.orsoncharts.axis.Axis3DChangeListener;
import com.orsoncharts.axis.CategoryAxis3D;
import com.orsoncharts.axis.ValueAxis3D;
import com.orsoncharts.data.Dataset3DChangeEvent;
import com.orsoncharts.data.ItemKey;
import com.orsoncharts.data.KeyedValues3DItemKey;
import com.orsoncharts.data.category.CategoryDataset3D;
import com.orsoncharts.graphics3d.Dimension3D;
import com.orsoncharts.graphics3d.World;
import com.orsoncharts.label.CategoryLabelGenerator;
import com.orsoncharts.label.CategoryItemLabelGenerator;
import com.orsoncharts.label.StandardCategoryLabelGenerator;
import com.orsoncharts.label.StandardCategoryItemLabelGenerator;
import com.orsoncharts.legend.LegendItemInfo;
import com.orsoncharts.legend.StandardLegendItemInfo;
import com.orsoncharts.renderer.Renderer3DChangeEvent;
import com.orsoncharts.renderer.Renderer3DChangeListener;
import com.orsoncharts.renderer.category.CategoryRenderer3D;
import com.orsoncharts.util.ArgChecks;
import com.orsoncharts.util.ObjectUtils;
import com.orsoncharts.util.SerialUtils;

/**
 * A 3D plot with two category axes (x and z) and a numerical y-axis that can
 * display data from a {@link CategoryDataset3D}.
 * <br><br>
 * The plot implements several listener interfaces so that it can receive 
 * notification of changes to its dataset, axes and renderer.  When change
 * events are received, the plot passes on a {@link Plot3DChangeEvent} to the
 * {@link Chart3D} instance that owns the plot.  This event chain is the 
 * mechanism that ensures that charts are repainted whenever the dataset 
 * changes, or when changes are made to the configuration of any chart 
 * component.
 * <br><br>
 * NOTE: This class is serializable, but the serialization format is subject 
 * to change in future releases and should not be relied upon for persisting 
 * instances of this class. 
 */
@SuppressWarnings("serial")
public class CategoryPlot3D extends AbstractPlot3D 
        implements Axis3DChangeListener, Renderer3DChangeListener, 
        Serializable {

    private static Stroke DEFAULT_GRIDLINE_STROKE = new BasicStroke(0.5f, 
            BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND, 1f, 
            new float[] { 3f, 3f }, 0f);

    /** The dataset. */
    private CategoryDataset3D dataset;
    
    /** The renderer (never {@code null}). */
    private CategoryRenderer3D renderer;

    /** The row axis. */
    private CategoryAxis3D rowAxis;
    
    /** The column axis. */
    private CategoryAxis3D columnAxis;
    
    /** The value axis. */
    private ValueAxis3D valueAxis;

    /** Are gridlines shown for the row (z) axis? */
    private boolean gridlinesVisibleForRows;
    
    /** The paint for the row axis gridlines (never {@code null}). */
    private transient Paint gridlinePaintForRows;
    
    /** The stroke for the row axis gridlines (never {@code null}). */
    private transient Stroke gridlineStrokeForRows;

    /** Are gridlines shown for the column (x) axis? */
    private boolean gridlinesVisibleForColumns;
    
    /** The paint for the column axis gridlines (never {@code null}). */
    private transient Paint gridlinePaintForColumns;
    
    /** The stroke for the column axis gridlines (never {@code null}). */
    private transient Stroke gridlineStrokeForColumns;

    /** Are gridlines shown for the value axis? */
    private boolean gridlinesVisibleForValues;
    
    /** The paint for the value axis gridlines (never {@code null}). */
    private transient Paint gridlinePaintForValues;

    /** The stroke for the value axis gridlines (never {@code null}). */
    private transient Stroke gridlineStrokeForValues;
    
    /** The legend label generator. */
    private CategoryLabelGenerator legendLabelGenerator;

    /** 
     * A special attribute to provide control over the y-dimension for the
     * plot when the plot dimensions are auto-calculated.  The default value
     * is {@code null}.
     * 
     * @since 1.2
     */
    private Double yDimensionOverride;
    
    /** 
     * The tool tip generator (if null there will be no tool tips).
     * 
     * @since 1.3
     */
    private CategoryItemLabelGenerator toolTipGenerator;
    
    /**
     * Creates a new plot with the supplied dataset, renderer and axes.
     * 
     * @param dataset  the dataset ({@code null} not permitted). 
     * @param renderer  the renderer ({@code null} not permitted).
     * @param rowAxis  the row axis ({@code null} not permitted).
     * @param columnAxis  the column axis ({@code null} not permitted).
     * @param valueAxis  the value axis ({@code null} not permitted).
     */
    public CategoryPlot3D(CategoryDataset3D dataset, 
            CategoryRenderer3D renderer, CategoryAxis3D rowAxis, 
            CategoryAxis3D columnAxis, ValueAxis3D valueAxis) {
        ArgChecks.nullNotPermitted(dataset, "dataset");
        ArgChecks.nullNotPermitted(renderer, "renderer");
        ArgChecks.nullNotPermitted(rowAxis, "rowAxis");
        ArgChecks.nullNotPermitted(columnAxis, "columnAxis");
        ArgChecks.nullNotPermitted(valueAxis, "valueAxis");
        this.dataset = dataset;
        this.dataset.addChangeListener(this);
        this.dimensions = calculateDimensions();
        this.renderer = renderer;
        this.renderer.setPlot(this);
        this.renderer.addChangeListener(this);
        this.rowAxis = rowAxis;
        this.rowAxis.addChangeListener(this);
        this.columnAxis = columnAxis;
        this.columnAxis.addChangeListener(this);
        this.valueAxis = valueAxis;
        this.valueAxis.addChangeListener(this);
        this.rowAxis.configureAsRowAxis(this);
        this.columnAxis.configureAsColumnAxis(this);
        this.valueAxis.configureAsValueAxis(this);
        this.gridlinesVisibleForValues = true;
        this.gridlinesVisibleForColumns = false;
        this.gridlinesVisibleForRows = false;
        this.gridlinePaintForRows = Color.WHITE;
        this.gridlinePaintForColumns = Color.WHITE;
        this.gridlinePaintForValues = Color.WHITE;
        this.gridlineStrokeForRows = DEFAULT_GRIDLINE_STROKE;
        this.gridlineStrokeForColumns = DEFAULT_GRIDLINE_STROKE;
        this.gridlineStrokeForValues = DEFAULT_GRIDLINE_STROKE;
        this.legendLabelGenerator = new StandardCategoryLabelGenerator();
        this.yDimensionOverride = null;
        this.toolTipGenerator = new StandardCategoryItemLabelGenerator();
    }
    
    /**
     * Sets the flag that controls whether the plot's dimensions are 
     * automatically calculated and, if {@code true}, sends a change
     * event to all registered listeners.
     * 
     * @param auto  the new flag value.
     * 
     * @since 1.2
     */
    public void setAutoAdjustDimensions(boolean auto) {
        this.autoAdjustDimensions = auto;
        if (auto) {
            this.dimensions = calculateDimensions();
            fireChangeEvent(true);
        }
    }
    
    /**
     * Sets the dimensions (in 3D space) for the plot, resets the 
     * {@code autoAdjustDimensions} flag to {@code false}, and sends
     * a {@link Plot3DChangeEvent} to all registered listeners.
     * 
     * @param dimensions  the dimensions ({@code null} not permitted).
     * 
     * @see Plot3D#getDimensions() 
     */
    public void setDimensions(Dimension3D dimensions) {
        ArgChecks.nullNotPermitted(dimensions, "dimensions");
        this.dimensions = dimensions;
        this.autoAdjustDimensions = false;
        fireChangeEvent(true);
    }

    /**
     * Returns the dataset for the chart.
     * 
     * @return The dataset (never {@code null}). 
     */
    public CategoryDataset3D getDataset() {
        return this.dataset;
    }
    
    /**
     * Sets the dataset and sends a {@link Plot3DChangeEvent} to all registered
     * listeners.  When you call this method, the axes will be reconfigured for 
     * the new data.
     * 
     * @param dataset  the dataset ({@code null} not permitted). 
     */
    public void setDataset(CategoryDataset3D dataset) {
        ArgChecks.nullNotPermitted(dataset, "dataset");
        this.dataset.removeChangeListener(this);
        this.dataset = dataset;
        this.dataset.addChangeListener(this);
        // we send ourselves a dataset change event since this will 
        // reconfigure the axes then trigger the required plot change event
        datasetChanged(new Dataset3DChangeEvent(this, this.dataset));
    }
    
    /**
     * Returns the renderer (very often you will need to cast this to a 
     * specific class to make customisations).
     * 
     * @return The renderer (never {@code null}). 
     */
    public CategoryRenderer3D getRenderer() {
        return this.renderer;
    }
    
    /**
     * Sets the renderer and sends a change event to all registered listeners.
     * 
     * @param renderer  the renderer ({@code null} not permitted).
     */
    public void setRenderer(CategoryRenderer3D renderer) {
        ArgChecks.nullNotPermitted(renderer, "renderer");
        this.renderer.removeChangeListener(this);
        this.renderer = renderer;
        this.renderer.addChangeListener(this);
        // a new renderer might mean the axis range needs changing...
        this.valueAxis.configureAsValueAxis(this);
        fireChangeEvent(true);
    }
    
    /**
     * Returns the row axis.
     * 
     * @return The row axis. 
     */
    public CategoryAxis3D getRowAxis() {
        return this.rowAxis;
    }
    
    /**
     * Sets the row axis and sends a {@link Plot3DChangeEvent} to all 
     * registered listeners.  The row axis is equivalent to the z-axis.
     * 
     * @param axis  the row axis ({@code null} not permitted).
     */
    public void setRowAxis(CategoryAxis3D axis) {
        ArgChecks.nullNotPermitted(axis, "axis");
        this.rowAxis.removeChangeListener(this);
        this.rowAxis = axis;
        this.rowAxis.addChangeListener(this);
        fireChangeEvent(true);
    }
    
    /**
     * Returns the column axis.
     * 
     * @return The column axis (never {@code null}).
     */
    public CategoryAxis3D getColumnAxis() {
        return this.columnAxis;
    }
    
    /**
     * Sets the column axis and sends a {@link Plot3DChangeEvent} to all 
     * registered listeners.
     * 
     * @param axis  the new axis ({@code null} not permitted).
     * 
     * @see #setRowAxis(com.orsoncharts.axis.CategoryAxis3D) 
     * @see #setValueAxis(com.orsoncharts.axis.ValueAxis3D) 
     * 
     */
    public void setColumnAxis(CategoryAxis3D axis) {
        ArgChecks.nullNotPermitted(axis, "axis");
        this.columnAxis.removeChangeListener(this);
        this.columnAxis = axis;
        this.columnAxis.addChangeListener(this);
        fireChangeEvent(true);
    }
    
    /**
     * Returns the value axis (the vertical axis in the plot).
     * 
     * @return The value axis (never {@code null}). 
     */
    public ValueAxis3D getValueAxis() {
        return this.valueAxis;
    }
    
    /**
     * Sets the value axis and sends a {@link Plot3DChangeEvent} to all 
     * registered listeners.
     * 
     * @param axis  the axis ({@code null} not permitted). 
     */
    public void setValueAxis(ValueAxis3D axis) {
        ArgChecks.nullNotPermitted(axis, "axis");
        this.valueAxis.removeChangeListener(this);
        this.valueAxis = axis;
        this.valueAxis.configureAsValueAxis(this);
        this.valueAxis.addChangeListener(this);
        fireChangeEvent(true);
    }
    
    /**
     * Returns {@code true} if gridlines are shown for the column axis
     * and {@code false} otherwise.  The default value is {@code false}.
     * 
     * @return A boolean. 
     */
    public boolean getGridlinesVisibleForRows() {
        return this.gridlinesVisibleForRows;
    }

    /**
     * Sets the flag that controls whether or not gridlines are shown for the
     * row axis and sends a {@link Plot3DChangeEvent} to all registered 
     * listeners.
     * 
     * @param visible  the new flag value.
     */
    public void setGridlinesVisibleForRows(boolean visible) {
        this.gridlinesVisibleForRows = visible;
        fireChangeEvent(false);
    }

    /**
     * Returns the paint used to draw the gridlines for the row axis, if they
     * are visible.
     * 
     * @return The paint (never {@code null}). 
     */
    public Paint getGridlinePaintForRows() {
        return this.gridlinePaintForRows;
    }

    /**
     * Sets the paint used for the row axis gridlines and sends a 
     * {@link Plot3DChangeEvent} to all registered listeners.
     * 
     * @param paint  the paint ({@code null} not permitted). 
     */
    public void setGridlinePaintForRows(Paint paint) {
        ArgChecks.nullNotPermitted(paint, "paint");
        this.gridlinePaintForRows = paint;
        fireChangeEvent(false);
    }

    /**
     * Returns the stroke for the gridlines associated with the row axis.
     * The default value is {@code BasicStroke(0.5f, BasicStroke.CAP_ROUND, 
     * BasicStroke.JOIN_ROUND, 1f, new float[] { 3f, 3f }, 0f)}.
     * 
     * @return The stroke (never {@code null}).
     */
    public Stroke getGridlineStrokeForRows() {
        return this.gridlineStrokeForRows;
    }

    /**
     * Sets the stroke used to draw the gridlines for the row axis, if they
     * are visible, and sends a {@link Plot3DChangeEvent} to all 
     * registered listeners.
     * 
     * @param stroke  the stroke ({@code null} not permitted). 
     */
    public void setGridlineStrokeForRows(Stroke stroke) {
        ArgChecks.nullNotPermitted(stroke, "stroke");
        this.gridlineStrokeForRows = stroke;
        fireChangeEvent(false);
    }

    /**
     * Returns {@code true} if gridlines are shown for the column axis
     * and {@code false} otherwise.  The default value is {@code false}.
     * 
     * @return A boolean. 
     */
    public boolean getGridlinesVisibleForColumns() {
        return this.gridlinesVisibleForColumns;
    }

    /**
     * Sets the flag that controls whether or not gridlines are shown for the
     * column axis and sends a {@link Plot3DChangeEvent} to all registered 
     * listeners.
     * 
     * @param visible  the new flag value.
     */
    public void setGridlinesVisibleForColumns(boolean visible) {
        this.gridlinesVisibleForColumns = visible;
        fireChangeEvent(false);
    }
    
    /**
     * Returns {@code true} if gridlines are shown for the column axis
     * and {@code false} otherwise.  The default value is {@code true}.
     * 
     * @return A boolean. 
     */
    public boolean getGridlinesVisibleForValues() {
        return this.gridlinesVisibleForValues;
    }
    
    /**
     * Sets the flag that controls whether or not gridlines are shown for the
     * value axis and sends a {@link Plot3DChangeEvent} to all registered 
     * listeners.
     * 
     * @param visible  the new flag value.
     */
    public void setGridlinesVisibleForValues(boolean visible) {
        this.gridlinesVisibleForValues = visible;
        fireChangeEvent(false);
    }

    /**
     * Returns the paint for the gridlines associated with the value axis. 
     * The default value is {@code Color.WHITE}.
     * 
     * @return The paint for value axis gridlines (never {@code null}). 
     */
    public Paint getGridlinePaintForValues() {
        return this.gridlinePaintForValues;
    }
    
    /**
     * Sets the paint used for the value axis gridlines and sends a 
     * {@link Plot3DChangeEvent} to all registered listeners.
     * 
     * @param paint  the paint ({@code null} not permitted). 
     */
    public void setGridlinePaintForValues(Paint paint) {
        ArgChecks.nullNotPermitted(paint, "paint");
        this.gridlinePaintForValues = paint;
        fireChangeEvent(false);
    }

    /**
     * Returns the stroke for the gridlines associated with the value axis.
     * The default value is {@code BasicStroke(0.5f, BasicStroke.CAP_ROUND, 
     * BasicStroke.JOIN_ROUND, 1f, new float[] { 3f, 3f }, 0f)}.
     * 
     * @return The stroke (never {@code null}).
     */
    public Stroke getGridlineStrokeForValues() {
        return this.gridlineStrokeForValues;
    }
    
    /**
     * Sets the stroke used to draw the grid lines for the value axis, if
     * they are visible, and sends a {@link Plot3DChangeEvent} to all
     * registered listeners.
     * 
     * @param stroke  the stroke ({@code null} not permitted).
     */
    public void setGridlineStrokeForValues(Stroke stroke) {
        ArgChecks.nullNotPermitted(stroke, "stroke");
        this.gridlineStrokeForValues = stroke;
        fireChangeEvent(false);
    }
    
    /**
     * Returns the paint used to draw the grid lines for the column axis, if
     * they are visible.  The default value is {@code Color.WHITE}.
     * 
     * @return The paint (never {@code null}). 
     */
    public Paint getGridlinePaintForColumns() {
        return this.gridlinePaintForColumns;
    }
    
    /**
     * Sets the paint used to draw the grid lines for the column axis, if 
     * they are visible, and sends a {@link Plot3DChangeEvent} to all 
     * registered listeners.
     * 
     * @param paint  the paint ({@code null} not permitted). 
     */
    public void setGridlinePaintForColumns(Paint paint) {
        ArgChecks.nullNotPermitted(paint, "paint");
        this.gridlinePaintForColumns = paint;
        fireChangeEvent(false);
    }

    /**
     * Returns the stroke for the gridlines associated with the column axis.
     * The default value is {@code BasicStroke(0.5f, BasicStroke.CAP_ROUND, 
     * BasicStroke.JOIN_ROUND, 1f, new float[] { 3f, 3f }, 0f)}.
     * 
     * @return The stroke (never {@code null}).
     */
    public Stroke getGridlineStrokeForColumns() {
        return this.gridlineStrokeForColumns;
    }
    
    /**
     * Sets the stroke used to draw the grid lines for the column axis, if
     * they are visible, and sends a {@link Plot3DChangeEvent} to all
     * registered listeners.
     * 
     * @param stroke  the stroke ({@code null} not permitted).
     */
    public void setGridlineStrokeForColumns(Stroke stroke) {
        ArgChecks.nullNotPermitted(stroke, "stroke");
        this.gridlineStrokeForColumns = stroke;
        fireChangeEvent(false);
    }

    /**
     * Returns the legend label generator, an object that converts key values
     * in the dataset into corresponding strings for presentation in the chart.
     * 
     * @return The legend label generator (never {@code null}).
     * 
     * @since 1.2
     */
    public CategoryLabelGenerator getLegendLabelGenerator() {
        return this.legendLabelGenerator;    
    }
    
    /**
     * Sets the legend label generator and sends a {@link Plot3DChangeEvent}
     * to all registered listeners.
     * 
     * @param generator  the generator ({@code null} not permitted).
     * 
     * @since 1.2
     */
    public void setLegendLabelGenerator(CategoryLabelGenerator generator) {
        ArgChecks.nullNotPermitted(generator, "generator");
        this.legendLabelGenerator = generator;
        fireChangeEvent(false);
    }
    
    /**
     * Returns the y-dimension override.  The default value is {@code null},
     * which means that when the plot dimensions are automatically calculated,
     * the height of the plot will be set to the greater of the width and
     * the depth.
     * 
     * @return The y-dimension override (possibly {@code null}). 
     * 
     * @since 1.2
     */
    public Double getYDimensionOverride() {
        return this.yDimensionOverride;
    }

    /**
     * Sets the y-dimension override and, if the {@code autoAdjustDimensions} 
     * flag is set, recalculates the dimensions and sends a 
     * {@link Plot3DChangeEvent} to all registered listeners.
     * 
     * @param dim  the new y-dimension override ({@code null} permitted).
     * 
     * @since 1.2
     */
    public void setYDimensionOverride(Double dim) {
        this.yDimensionOverride = dim;
        if (this.autoAdjustDimensions) {
            this.dimensions = calculateDimensions();
            fireChangeEvent(true);
        }
    }
    
    /**
     * Returns the tool tip generator.  This is an object that calculates and
     * returns a string (that will be used as the tool tip) for any given
     * data value in the dataset.
     * 
     * @return The tool tip generator (possibly {@code null}).
     * 
     * @since 1.3
     */
    public CategoryItemLabelGenerator getToolTipGenerator() {
        return this.toolTipGenerator;
    }
    
    /**
     * Sets the tool tip generator and sends a {@link Plot3DChangeEvent} to all 
     * registered listeners.
     * 
     * @param generator  the new generator ({@code null} permitted).
     * 
     * @since 1.3
     */
    public void setToolTipGenerator(CategoryItemLabelGenerator generator) {
        this.toolTipGenerator = generator;
        fireChangeEvent(false);
    }

    /**
     * Returns a list containing legend item info, typically one item for
     * each series in the chart.  This is intended for use in the construction
     * of a chart legend.
     * 
     * @return A list containing legend item info (possibly empty but never 
     *     {@code null}).
     */
    @Override
    public List<LegendItemInfo> getLegendInfo() {
        List<LegendItemInfo> result = new ArrayList<LegendItemInfo>();
        List<Comparable<?>> keys = this.dataset.getSeriesKeys();
        for (Comparable<?> key : keys) {
            int series = this.dataset.getSeriesIndex(key);
            Color color = this.renderer.getColorSource().getLegendColor(series);
            String seriesLabel = this.legendLabelGenerator.generateSeriesLabel(
                    this.dataset, key);
            LegendItemInfo info = new StandardLegendItemInfo(key, 
                    seriesLabel, color);
            result.add(info);
        }
        return result;
    }

    @Override
    public void compose(World world, double xOffset, double yOffset, 
            double zOffset) {
        for (int series = 0; series < this.dataset.getSeriesCount(); series++) {
            for (int row = 0; row < this.dataset.getRowCount(); row++) {
                for (int column = 0; column < this.dataset.getColumnCount(); 
                        column++) {
                    this.renderer.composeItem(this.dataset, series, row, column,
                            world, getDimensions(), xOffset, yOffset, zOffset);
                }
            }
        }
    }
    
    @Override
    public String generateToolTipText(ItemKey itemKey) {
        if (!(itemKey instanceof KeyedValues3DItemKey)) {
            throw new IllegalArgumentException(
                    "The itemKey must be a Values3DItemKey instance.");
        }
        KeyedValues3DItemKey vik = (KeyedValues3DItemKey) itemKey;
        return this.toolTipGenerator.generateItemLabel(dataset, 
                vik.getSeriesKey(), vik.getRowKey(), vik.getColumnKey());
    }

    /**
     * Accepts a visitor for the plot.  This method first calls the 
     * {@code receive()} method for each of the plot's axes and the renderer, 
     * then performs the visitor's function on the plot.  This is a general 
     * purpose mechanism, but the main use is to apply chart style changes 
     * across all the elements of a chart.
     * 
     * @param visitor  the visitor ({@code null} not permitted).
     * 
     * @since 1.2
     */
    @Override 
    public void receive(ChartElementVisitor visitor) {
        this.columnAxis.receive(visitor);
        this.rowAxis.receive(visitor);
        this.valueAxis.receive(visitor);
        this.renderer.receive(visitor);
        visitor.visit(this);
    }

    /**
     * Tests this plot for equality with an arbitrary object.
     * 
     * @param obj  the object ({@code null} permitted).
     * 
     * @return A boolean. 
     */
    @Override
    public boolean equals(Object obj) {
        if (obj == this) {
            return true;
        }    
        if (!(obj instanceof CategoryPlot3D)) {
            return false;
        }
        CategoryPlot3D that = (CategoryPlot3D) obj;
        if (this.gridlinesVisibleForRows != that.gridlinesVisibleForRows) {
            return false;
        }
        if (!this.gridlineStrokeForRows.equals(that.gridlineStrokeForRows)) {
            return false;
        }
        if (!ObjectUtils.equalsPaint(this.gridlinePaintForRows, 
                that.gridlinePaintForRows)) {
            return false;
        }
        if (this.gridlinesVisibleForColumns 
                != that.gridlinesVisibleForColumns) {
            return false;
        }
        if (!this.gridlineStrokeForColumns.equals(
                that.gridlineStrokeForColumns)) {
            return false;
        }
        if (!ObjectUtils.equalsPaint(this.gridlinePaintForColumns, 
                that.gridlinePaintForColumns)) {
            return false;
        }
        if (this.gridlinesVisibleForValues != that.gridlinesVisibleForValues) {
            return false;
        }
        if (!this.gridlineStrokeForValues.equals(that.gridlineStrokeForValues)) {
            return false;
        }
        if (!ObjectUtils.equalsPaint(this.gridlinePaintForValues, 
                 that.gridlinePaintForValues)) {
            return false;
        }
        if (!this.legendLabelGenerator.equals(that.legendLabelGenerator)) {
            return false;
        }
        if (!ObjectUtils.equals(this.yDimensionOverride, 
                that.yDimensionOverride)) {
            return false;
        }
        if (!ObjectUtils.equals(this.toolTipGenerator, that.toolTipGenerator)) {
            return false;
        }
       return super.equals(obj);
    }
    
    /**
     * Receives notification of a change to the dataset and handles this by
     * adjusting the plot dimensions (according to the setting of the
     * {@code autoAdjustDimensions} flag), reconfiguring the axes, and
     * propagating a {@code Plot3DChangeEvent}. 
     * 
     * @param event  the change event. 
     */
    @Override
    public void datasetChanged(Dataset3DChangeEvent event) {
        // update the category axis labels 
        // and the value axis range
        if (this.autoAdjustDimensions) {
            this.dimensions = calculateDimensions();
        }
        this.columnAxis.configureAsColumnAxis(this);
        this.rowAxis.configureAsRowAxis(this);
        this.valueAxis.configureAsValueAxis(this);
        super.datasetChanged(event);  // propagates a plot change event
    }
    
    /**
     * Returns the dimensions for the plot that best suit the current data 
     * values.  The x-dimension is set to the number of columns in the 
     * dataset and the z-dimension is set to the number of rows in the dataset.
     * For the y-dimension, the code first checks the 
     * {@code yDimensionOverride} attribute to see if a specific value is 
     * requested...and if not, the minimum of the x and z dimensions will be
     * used.
     * 
     * @return The dimensions (never {@code null}). 
     */
    private Dimension3D calculateDimensions() {
        double depth = Math.max(1.0, this.dataset.getRowCount() + 1);
        double width = Math.max(1.0, this.dataset.getColumnCount() + 1);
        double height = Math.max(1.0, Math.min(width, depth));
        if (this.yDimensionOverride != null) {
            height = this.yDimensionOverride.doubleValue();
        }
        return new Dimension3D(width, height, depth);
    }
   
    /**
     * Receives notification that one of the axes has been changed.  This will
     * trigger a {@link Plot3DChangeEvent} that will usually cause the chart
     * to be repainted.
     * 
     * @param event  the change event. 
     */
    @Override
    public void axisChanged(Axis3DChangeEvent event) {
        // for now we just fire a plot change event which will flow up the
        // chain and eventually trigger a chart repaint
        fireChangeEvent(event.requiresWorldUpdate());
    }

    /**
     * Receives notification that the renderer has been modified in some way.  
     * This will trigger a {@link Plot3DChangeEvent} that will usually cause 
     * the chart to be repainted.     
     * 
     * @param event  information about the event. 
     */
    @Override
    public void rendererChanged(Renderer3DChangeEvent event) {
        // for now we just fire a plot change event which will flow up the
        // chain and eventually trigger a chart repaint
        fireChangeEvent(event.requiresWorldUpdate());
    }
    
    /**
     * Provides serialization support.
     *
     * @param stream  the output stream.
     *
     * @throws IOException  if there is an I/O error.
     */
    private void writeObject(ObjectOutputStream stream) throws IOException {
        stream.defaultWriteObject();
        SerialUtils.writePaint(this.gridlinePaintForRows, stream);
        SerialUtils.writePaint(this.gridlinePaintForColumns, stream);
        SerialUtils.writePaint(this.gridlinePaintForValues, stream);
        SerialUtils.writeStroke(this.gridlineStrokeForRows, stream);
        SerialUtils.writeStroke(this.gridlineStrokeForColumns, stream);
        SerialUtils.writeStroke(this.gridlineStrokeForValues, stream);
    }

    /**
     * Provides serialization support.
     *
     * @param stream  the input stream.
     *
     * @throws IOException  if there is an I/O error.
     * @throws ClassNotFoundException  if there is a classpath problem.
     */
    private void readObject(ObjectInputStream stream)
        throws IOException, ClassNotFoundException {
        stream.defaultReadObject();
        this.gridlinePaintForRows = SerialUtils.readPaint(stream);
        this.gridlinePaintForColumns = SerialUtils.readPaint(stream);
        this.gridlinePaintForValues = SerialUtils.readPaint(stream);
        this.gridlineStrokeForRows = SerialUtils.readStroke(stream);
        this.gridlineStrokeForColumns = SerialUtils.readStroke(stream);
        this.gridlineStrokeForValues = SerialUtils.readStroke(stream);
    }

}
