/* ===========================================================
 * 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.Color;
import java.awt.Font;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;

import com.orsoncharts.Chart3D;
import com.orsoncharts.data.PieDataset3D;
import com.orsoncharts.util.ArgChecks;
import com.orsoncharts.graphics3d.Dimension3D;
import com.orsoncharts.graphics3d.Dot3D;
import com.orsoncharts.graphics3d.Object3D;
import com.orsoncharts.graphics3d.World;
import com.orsoncharts.legend.LegendItemInfo;
import com.orsoncharts.legend.StandardLegendItemInfo;
import com.orsoncharts.Chart3DFactory;
import com.orsoncharts.ChartElementVisitor;
import com.orsoncharts.data.DataUtils;
import com.orsoncharts.data.ItemKey;
import com.orsoncharts.data.KeyedValuesItemKey;
import com.orsoncharts.label.PieLabelGenerator;
import com.orsoncharts.label.StandardPieLabelGenerator;

/**
 * A plot for creating 3D pie charts.  To create a pie chart, you can use the 
 * {@code createPieChart()} method in the {@link Chart3DFactory} class.  
 * A typical pie chart will look like this:  
 * <div>
 * <object id="ABC" data="../../../doc-files/PieChart3DDemo1.svg"  
 * type="image/svg+xml" width="500" height="359"> 
 * </object>
 * </div>
 * <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 PiePlot3D extends AbstractPlot3D implements Serializable {
    
    /** The default font for section labels on the chart. */
    public static final Font DEFAULT_SECTION_LABEL_FONT 
            = new Font("Dialog", Font.PLAIN, 14);
    
    /** The dataset. */
    private PieDataset3D dataset;

    /** The radius of the pie chart. */
    private double radius; 
  
    /** The depth of the pie chart. */
    private double depth;
  
    /** The section color source. */
    private ColorSource sectionColorSource;

    /** The section label generator. */
    private PieLabelGenerator sectionLabelGenerator;
    
    /** The font source used to determine the font for section labels. */
    private FontSource sectionLabelFontSource;

    /** 
     * The color source used to determine the foreground color for section 
     * labels. 
     */
    private ColorSource sectionLabelColorSource;
    
    /** The legend label generator. */
    private PieLabelGenerator legendLabelGenerator;
    
    /** 
     * The tool tip generator (can be null, in which case there will be no
     * tool tips. */
    private PieLabelGenerator toolTipGenerator;
    
    /** 
     * The number of segments used to render 360 degrees of the pie.  A higher
     * number will give better output but slower performance.
     */
    private int segments = 40;
  
    /**
     * Creates a new pie plot in 3D.
     * 
     * @param dataset  the dataset ({@code null} not permitted). 
     */
    public PiePlot3D(PieDataset3D dataset) {
        ArgChecks.nullNotPermitted(dataset, "dataset");
        this.dataset = dataset;
        this.dataset.addChangeListener(this);
        this.radius = 4.0;    
        this.depth = 0.5;
        this.sectionColorSource = new StandardColorSource();
        this.sectionLabelGenerator = new StandardPieLabelGenerator(
                StandardPieLabelGenerator.KEY_ONLY_TEMPLATE);
        this.sectionLabelFontSource = new StandardFontSource(
                DEFAULT_SECTION_LABEL_FONT);
        this.sectionLabelColorSource = new StandardColorSource(Color.BLACK);
        this.legendLabelGenerator = new StandardPieLabelGenerator();
        this.toolTipGenerator = new StandardPieLabelGenerator(
                StandardPieLabelGenerator.PERCENT_TEMPLATE_2DP);
    }

    /**
     * Returns the dataset.
     * 
     * @return The dataset (never {@code null}). 
     */
    public PieDataset3D getDataset() {
        return this.dataset;
    }

    /**
     * Sets the dataset and notifies registered listeners that the dataset has
     * been updated.
     * 
     * @param dataset  the dataset ({@code null} not permitted). 
     */
    public void setDataset(PieDataset3D dataset) {
        ArgChecks.nullNotPermitted(dataset, "dataset");
        this.dataset.removeChangeListener(this);
        this.dataset = dataset;
        this.dataset.addChangeListener(this);
        fireChangeEvent(true);
    }

    /**
     * Returns the radius of the pie (the default value is 8.0).
     * 
     * @return The radius of the pie. 
     */
    public double getRadius() {
        return this.radius;
    }
  
    /**
     * Sets the radius of the pie chart and sends a change event to all 
     * registered listeners.
     * 
     * @param radius  the radius. 
     */
    public void setRadius(double radius) {
        this.radius = radius;
        fireChangeEvent(true);
    }
  
    /**
     * Returns the depth of the pie (the default value is 2.0).
     * 
     * @return The depth of the pie. 
     */
    public double getDepth() {
        return this.depth;
    }

    /**
     * Sets the depth of the pie chart and sends a change event to all 
     * registered listeners.
     * 
     * @param depth  the depth. 
     */
    public void setDepth(double depth) {
        this.depth = depth;
        fireChangeEvent(true);
    }
    
    /**
     * Returns the color source for section colors.
     * 
     * @return The color source (never {@code null}).
     */
    public ColorSource getSectionColorSource() {
        return this.sectionColorSource;
    }
    
    /**
     * Sets the color source and sends a {@link Plot3DChangeEvent} to all 
     * registered listeners.
     * 
     * @param source  the color source ({@code null} not permitted). 
     */
    public void setSectionColorSource(ColorSource source) {
        ArgChecks.nullNotPermitted(source, "source");
        this.sectionColorSource = source;
        fireChangeEvent(true);
    }
    
    /**
     * Sets a new color source for the plot using the specified colors and
     * sends a {@link Plot3DChangeEvent} to all registered listeners. This 
     * is a convenience method that is equivalent to 
     * {@code setSectionColorSource(new StandardColorSource(colors))}.
     * 
     * @param colors  one or more colors ({@code null} not permitted).
     * 
     * @since 1.2
     */
    public void setSectionColors(Color... colors) {
        setSectionColorSource(new StandardColorSource(colors));
    }

    /**
     * Returns the object that creates labels for each section of the pie
     * chart.
     * 
     * @return The section label generator (never {@code null}).
     * 
     * @since 1.2
     */
    public PieLabelGenerator getSectionLabelGenerator() {
        return this.sectionLabelGenerator;    
    }
    
    /**
     * Sets the object that creates labels for each section of the pie chart,
     * and sends a {@link Plot3DChangeEvent} to all registered listeners.
     * 
     * @param generator  the generator ({@code null} not permitted).
     * 
     * @since 1.2
     */
    public void setSectionLabelGenerator(PieLabelGenerator generator) {
        ArgChecks.nullNotPermitted(generator, "generator");
        this.sectionLabelGenerator = generator;
        fireChangeEvent(false);
    }
    
    /**
     * Returns the font source that is used to determine the font to use for 
     * the section labels.
     * 
     * @return The font source for the section labels (never {@code null}). 
     */
    public FontSource getSectionLabelFontSource() {
        return this.sectionLabelFontSource; 
    }
    
    /**
     * Sets the font source and sends a {@link Plot3DChangeEvent} to all
     * registered listeners.
     * 
     * @param source  the source ({@code null} not permitted). 
     */
    public void setSectionLabelFontSource(FontSource source) {
        ArgChecks.nullNotPermitted(source, "source");
        this.sectionLabelFontSource = source;
        fireChangeEvent(false);
    }

    /**
     * Returns the color source for section labels.  The default value is
     * an instance of {@link StandardColorSource} that always returns
     * {@code Color.BLACK}.
     * 
     * @return The color source (never {@code null}).
     * 
     * @see #setSectionLabelColorSource(ColorSource) 
     */
    public ColorSource getSectionLabelColorSource() {
        return this.sectionLabelColorSource;
    }
    
    /**
     * Sets the color source for the section labels and sends a 
     * {@link Plot3DChangeEvent} to all registered listeners.
     * 
     * @param source  the color source. 
     * 
     * @see #getSectionLabelColorSource() 
     */
    public void setSectionLabelColorSource(ColorSource source) {
        ArgChecks.nullNotPermitted(source, "source");
        this.sectionLabelColorSource = source;
        fireChangeEvent(false);
    }
    
    /**
     * Returns the object that creates legend labels for each section of the pie
     * chart.
     * 
     * @return The legend label generator (never {@code null}).
     * 
     * @since 1.2
     */
    public PieLabelGenerator getLegendLabelGenerator() {
        return this.legendLabelGenerator;
    }
    
    /**
     * Sets the object that creates legend labels for each section of the pie 
     * chart, and sends a {@link Plot3DChangeEvent} to all registered 
     * listeners.
     * 
     * @param generator  the generator ({@code null} not permitted).
     * 
     * @since 1.2
     */
    public void setLegendLabelGenerator(PieLabelGenerator generator) {
        ArgChecks.nullNotPermitted(generator, "generator");
        this.legendLabelGenerator = generator;
        fireChangeEvent(false);
    }
    
    /**
     * Returns the tool tip generator.
     * 
     * @return The tool tip generator (possibly {@code null}).
     * 
     * @since 1.3
     */
    public PieLabelGenerator getToolTipGenerator() {
        return this.toolTipGenerator;
    }
    
    /**
     * Sets the tool tip generator and sends a change event to all registered
     * listeners.
     * 
     * @param generator  the generator ({@code null} permitted).
     * 
     * @since 1.3
     */
    public void setToolTipGenerator(PieLabelGenerator generator) {
        this.toolTipGenerator = generator;
        fireChangeEvent(false);
    }

    /**
     * Returns the dimensions for the plot.  For the pie chart, it is more 
     * natural to specify the dimensions in terms of a radius and a depth, so
     * we use those values to calculate the dimensions here.
     * 
     * @return The dimensions for the plot. 
     */
    @Override
    public Dimension3D getDimensions() {
        return new Dimension3D(this.radius * 2, this.depth, this.radius * 2);
    }
 
    /**
     * Returns the number of segments used when composing the 3D objects
     * representing the pie chart.  The default value is {@code 40}.
     * 
     * @return The number of segments used to compose the pie chart. 
     */
    public int getSegmentCount() {
        return this.segments;
    }
    
    /**
     * Sets the number of segments used when composing the pie chart and 
     * sends a {@link Plot3DChangeEvent} to all registered listeners.  A higher
     * number will result in a more rounded pie chart, but will take longer
     * to render.
     * 
     * @param count  the count. 
     */
    public void setSegmentCount(int count) {
        this.segments = count;
        fireChangeEvent(true);
    }
    
    /**
     * 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.
     */
    @Override
    public List<LegendItemInfo> getLegendInfo() {
        List<LegendItemInfo> result = new ArrayList<LegendItemInfo>();
        for (Comparable<?> key : this.dataset.getKeys()) {
            String label = this.legendLabelGenerator.generateLabel(dataset, 
                    key);
            LegendItemInfo info = new StandardLegendItemInfo(key, 
                    label, this.sectionColorSource.getColor(key));
            result.add(info);
        }
        return result;
    }
    
    /**
     * Adds 3D objects representing the current data for the plot to the 
     * specified world.  After the world has been populated (or constructed) in
     * this way, it is ready for rendering.  This method is called by the
     * {@link Chart3D} class, you won't normally call it directly.
     * 
     * @param world  the world ({@code null} not permitted).
     * @param xOffset  the x-offset.
     * @param yOffset  the y-offset.
     * @param zOffset  the z-offset.
     */
    @Override
    public void compose(World world, double xOffset, double yOffset, 
            double zOffset) {
        double total = DataUtils.total(this.dataset);
        double r = 0.0;
        int count = this.dataset.getItemCount();
        for (int i = 0; i < count; i++) {
            Comparable<?> key = this.dataset.getKey(i);
            Number n = this.dataset.getValue(i);
            if (n != null) {
                double angle = Math.PI * 2 * (n.doubleValue() / total);
                Color c = this.sectionColorSource.getColor(
                        this.dataset.getKey(i));
                Object3D segment = Object3D.createPieSegment(this.radius, 0.0, 
                        yOffset, this.depth, r, r + angle, 
                        Math.PI / this.segments, c);
                segment.setProperty(Object3D.ITEM_KEY, 
                        new KeyedValuesItemKey(key));
                world.add(segment);
                r = r + angle;
            }
        }
    }
  
    /**
     * Returns a list of label faces for the plot.  These are non-visible 
     * objects added to the 3D model of the pie chart to track the positions 
     * for labels (which are added after the plot is projected and rendered).  
     * <br><br>
     * NOTE: This method is public so that it can be called by the 
     * {@link Chart3D} class - you won't normally call it directly.
     * 
     * @param xOffset  the x-offset.
     * @param yOffset  the y-offset.
     * @param zOffset  the z-offset.
     * 
     * @return A list of label faces.
     */
    public List<Object3D> getLabelFaces(double xOffset, double yOffset, 
            double zOffset) {
        double total = DataUtils.total(this.dataset);
        List<Object3D> result = new ArrayList<Object3D>();
        // this adds the centre points
        result.add(new Dot3D(0.0f, 0.0f, 0.0f, Color.RED));
        result.add(new Dot3D(0.0f, (float) yOffset, 0.0f, Color.RED));
        double r = 0.0;
        int count = this.dataset.getItemCount();
        for (int i = 0; i < count; i++) {
            Number n = this.dataset.getValue(i);
            double angle = 0.0;
            if (n != null) {
                angle = Math.PI * 2 * (n.doubleValue() / total);
            }
            result.addAll(Object3D.createPieLabelMarkers(this.radius * 1.2,
                    0.0, yOffset - this.depth * 0.05, this.depth * 1.1, r, 
                    r + angle));
            r = r + angle;
        }
        return result;
    }

    @Override
    public String generateToolTipText(ItemKey itemKey) {
        if (!(itemKey instanceof KeyedValuesItemKey)) {
            throw new IllegalArgumentException(
                    "The itemKey must be a ValuesItemKey instance.");
        }
        KeyedValuesItemKey vik = (KeyedValuesItemKey) itemKey;
        return this.toolTipGenerator.generateLabel(this.dataset, vik.getKey());
    }

    /**
     * Receives a visitor.  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) { 
        visitor.visit(this);
    }

    /**
     * Tests this plot for equality with an arbitrary object.  Note that the
     * plot's dataset is NOT considered in the equality test.
     * 
     * @param obj  the object ({@code null} not permitted).
     * 
     * @return A boolean. 
     */
    @Override
    public boolean equals(Object obj) {
        if (obj == this) {
            return true;
        }
        if (!(obj instanceof PiePlot3D)) {
            return false;
        }
        PiePlot3D that = (PiePlot3D) obj;
        if (this.radius != that.radius) {
            return false;
        }
        if (this.depth != that.depth) {
            return false;
        }
        if (!this.sectionColorSource.equals(that.sectionColorSource)) {
            return false;
        }
        if (!this.sectionLabelGenerator.equals(that.sectionLabelGenerator)) {
            return false;
        }
        if (!this.sectionLabelFontSource.equals(that.sectionLabelFontSource)) {
            return false;
        }
        if (!this.sectionLabelColorSource.equals(
                that.sectionLabelColorSource)) {
            return false;
        }
        if (!this.legendLabelGenerator.equals(that.legendLabelGenerator)) {
            return false;
        }
        if (!this.toolTipGenerator.equals(that.toolTipGenerator)) {
            return false;
        }
        if (this.segments != that.segments) {
            return false;
        }
        return super.equals(obj);
    }

    @Override
    public int hashCode() {
        int hash = 5;
        hash = 97 * hash + (int) (Double.doubleToLongBits(this.radius) 
                ^ (Double.doubleToLongBits(this.radius) >>> 32));
        hash = 97 * hash + (int) (Double.doubleToLongBits(this.depth) 
                ^ (Double.doubleToLongBits(this.depth) >>> 32));
        hash = 97 * hash + this.sectionColorSource.hashCode();
        hash = 97 * hash + this.sectionLabelGenerator.hashCode();
        hash = 97 * hash + this.sectionLabelFontSource.hashCode();
        hash = 97 * hash + this.sectionLabelColorSource.hashCode();
        hash = 97 * hash + this.segments;
        return hash;
    }

}
