/* ===========================================================
 * JFreeChart : a free chart library for the Java(tm) platform
 * ===========================================================
 *
 * (C) Copyright 2000-2007, by Object Refinery Limited and Contributors.
 *
 * Project Info:  http://www.jfree.org/jfreechart/index.html
 *
 * This library is free software; you can redistribute it and/or modify it 
 * under the terms of the GNU Lesser General Public License as published by 
 * the Free Software Foundation; either version 2.1 of the License, or 
 * (at your option) any later version.
 *
 * This library 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 Lesser General Public 
 * License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, 
 * USA.  
 *
 * [Java is a trademark or registered trademark of Sun Microsystems, Inc. 
 * in the United States and other countries.]
 *
 * ------------------
 * LegendGraphic.java
 * ------------------
 * (C) Copyright 2004-2007, by Object Refinery Limited.
 *
 * Original Author:  David Gilbert (for Object Refinery Limited);
 * Contributor(s):   -;
 *
 * Changes
 * -------
 * 26-Oct-2004 : Version 1 (DG);
 * 21-Jan-2005 : Modified return type of RectangleAnchor.coordinates() 
 *               method (DG);
 * 20-Apr-2005 : Added new draw() method (DG);
 * 13-May-2005 : Fixed to respect margin, border and padding settings (DG);
 * 01-Sep-2005 : Implemented PublicCloneable (DG);
 * ------------- JFREECHART 1.0.x ---------------------------------------------
 * 13-Dec-2006 : Added fillPaintTransformer attribute, so legend graphics can
 *               display gradient paint correctly, updated equals() and 
 *               corrected clone() (DG);
 * 01-Aug-2007 : Updated API docs (DG);
 * 
 */

package org.jfree.chart.title;

import java.awt.GradientPaint;
import java.awt.Graphics2D;
import java.awt.Paint;
import java.awt.Shape;
import java.awt.Stroke;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;

import org.jfree.chart.block.AbstractBlock;
import org.jfree.chart.block.Block;
import org.jfree.chart.block.LengthConstraintType;
import org.jfree.chart.block.RectangleConstraint;
import org.jfree.io.SerialUtilities;
import org.jfree.ui.GradientPaintTransformer;
import org.jfree.ui.RectangleAnchor;
import org.jfree.ui.Size2D;
import org.jfree.ui.StandardGradientPaintTransformer;
import org.jfree.util.ObjectUtilities;
import org.jfree.util.PaintUtilities;
import org.jfree.util.PublicCloneable;
import org.jfree.util.ShapeUtilities;

/**
 * The graphical item within a legend item.
 */
public class LegendGraphic extends AbstractBlock 
                           implements Block, PublicCloneable {
    
    /** For serialization. */
    static final long serialVersionUID = -1338791523854985009L;

    /** 
     * A flag that controls whether or not the shape is visible - see also 
     * lineVisible. 
     */
    private boolean shapeVisible;
    
    /** 
     * The shape to display.  To allow for accurate positioning, the center
     * of the shape should be at (0, 0). 
     */
    private transient Shape shape;
    
    /**
     * Defines the location within the block to which the shape will be aligned.
     */
    private RectangleAnchor shapeLocation;
    
    /** 
     * Defines the point on the shape's bounding rectangle that will be 
     * aligned to the drawing location when the shape is rendered.
     */
    private RectangleAnchor shapeAnchor;
    
    /** A flag that controls whether or not the shape is filled. */
    private boolean shapeFilled;
    
    /** The fill paint for the shape. */
    private transient Paint fillPaint;
    
    /**
     * The fill paint transformer (used if the fillPaint is an instance of
     * GradientPaint).
     * 
     * @since 1.0.4
     */
    private GradientPaintTransformer fillPaintTransformer;
    
    /** A flag that controls whether or not the shape outline is visible. */
    private boolean shapeOutlineVisible;
    
    /** The outline paint for the shape. */
    private transient Paint outlinePaint;
    
    /** The outline stroke for the shape. */
    private transient Stroke outlineStroke;
    
    /** 
     * A flag that controls whether or not the line is visible - see also 
     * shapeVisible. 
     */
    private boolean lineVisible;
    
    /** The line. */
    private transient Shape line;
    
    /** The line stroke. */
    private transient Stroke lineStroke;
    
    /** The line paint. */
    private transient Paint linePaint;
    
    /**
     * Creates a new legend graphic.
     * 
     * @param shape  the shape (<code>null</code> not permitted).
     * @param fillPaint  the fill paint (<code>null</code> not permitted).
     */
    public LegendGraphic(Shape shape, Paint fillPaint) {
        if (shape == null) {
            throw new IllegalArgumentException("Null 'shape' argument.");
        }
        if (fillPaint == null) {
            throw new IllegalArgumentException("Null 'fillPaint' argument.");
        }
        this.shapeVisible = true;
        this.shape = shape;
        this.shapeAnchor = RectangleAnchor.CENTER;
        this.shapeLocation = RectangleAnchor.CENTER;
        this.shapeFilled = true;
        this.fillPaint = fillPaint;
        this.fillPaintTransformer = new StandardGradientPaintTransformer();
        setPadding(2.0, 2.0, 2.0, 2.0);
    }
    
    /**
     * Returns a flag that controls whether or not the shape
     * is visible.
     * 
     * @return A boolean.
     * 
     * @see #setShapeVisible(boolean)
     */
    public boolean isShapeVisible() {
        return this.shapeVisible;
    }
    
    /**
     * Sets a flag that controls whether or not the shape is 
     * visible.
     * 
     * @param visible  the flag.
     * 
     * @see #isShapeVisible()
     */
    public void setShapeVisible(boolean visible) {
        this.shapeVisible = visible;
    }
    
    /**
     * Returns the shape.
     * 
     * @return The shape.
     * 
     * @see #setShape(Shape)
     */
    public Shape getShape() {
        return this.shape;
    }
    
    /**
     * Sets the shape.
     * 
     * @param shape  the shape.
     * 
     * @see #getShape()
     */
    public void setShape(Shape shape) {
        this.shape = shape;
    }

    /**
     * Returns a flag that controls whether or not the shapes
     * are filled.
     * 
     * @return A boolean.
     * 
     * @see #setShapeFilled(boolean)
     */
    public boolean isShapeFilled() {
        return this.shapeFilled;
    }
    
    /**
     * Sets a flag that controls whether or not the shape is
     * filled.
     * 
     * @param filled  the flag.
     * 
     * @see #isShapeFilled()
     */
    public void setShapeFilled(boolean filled) {
        this.shapeFilled = filled;
    }

    /**
     * Returns the paint used to fill the shape.
     * 
     * @return The fill paint.
     * 
     * @see #setFillPaint(Paint)
     */
    public Paint getFillPaint() {
        return this.fillPaint;
    }
    
    /**
     * Sets the paint used to fill the shape.
     * 
     * @param paint  the paint.
     * 
     * @see #getFillPaint()
     */
    public void setFillPaint(Paint paint) {
        this.fillPaint = paint;
    }
    
    /**
     * Returns the transformer used when the fill paint is an instance of 
     * <code>GradientPaint</code>.
     * 
     * @return The transformer (never <code>null</code>).
     * 
     * @since 1.0.4.
     * 
     * @see #setFillPaintTransformer(GradientPaintTransformer)
     */
    public GradientPaintTransformer getFillPaintTransformer() {
        return this.fillPaintTransformer;
    }
    
    /**
     * Sets the transformer used when the fill paint is an instance of 
     * <code>GradientPaint</code>.
     * 
     * @param transformer  the transformer (<code>null</code> not permitted).
     * 
     * @since 1.0.4
     * 
     * @see #getFillPaintTransformer()
     */
    public void setFillPaintTransformer(GradientPaintTransformer transformer) {
        if (transformer == null) {
            throw new IllegalArgumentException("Null 'transformer' argument.");
        }
        this.fillPaintTransformer = transformer;
    }
    
    /**
     * Returns a flag that controls whether the shape outline is visible.
     * 
     * @return A boolean.
     * 
     * @see #setShapeOutlineVisible(boolean)
     */
    public boolean isShapeOutlineVisible() {
        return this.shapeOutlineVisible;
    }
    
    /**
     * Sets a flag that controls whether or not the shape outline
     * is visible.
     * 
     * @param visible  the flag.
     * 
     * @see #isShapeOutlineVisible()
     */
    public void setShapeOutlineVisible(boolean visible) {
        this.shapeOutlineVisible = visible;
    }
    
    /**
     * Returns the outline paint.
     * 
     * @return The paint.
     * 
     * @see #setOutlinePaint(Paint)
     */
    public Paint getOutlinePaint() {
        return this.outlinePaint;
    }
    
    /**
     * Sets the outline paint.
     * 
     * @param paint  the paint.
     * 
     * @see #getOutlinePaint()
     */
    public void setOutlinePaint(Paint paint) {
        this.outlinePaint = paint;
    }

    /**
     * Returns the outline stroke.
     * 
     * @return The stroke.
     * 
     * @see #setOutlineStroke(Stroke)
     */
    public Stroke getOutlineStroke() {
        return this.outlineStroke;
    }
    
    /**
     * Sets the outline stroke.
     * 
     * @param stroke  the stroke.
     * 
     * @see #getOutlineStroke()
     */
    public void setOutlineStroke(Stroke stroke) {
        this.outlineStroke = stroke;
    }

    /**
     * Returns the shape anchor.
     * 
     * @return The shape anchor.
     * 
     * @see #getShapeAnchor()
     */
    public RectangleAnchor getShapeAnchor() {
        return this.shapeAnchor;
    }
    
    /**
     * Sets the shape anchor.  This defines a point on the shapes bounding
     * rectangle that will be used to align the shape to a location.
     * 
     * @param anchor  the anchor (<code>null</code> not permitted).
     * 
     * @see #setShapeAnchor(RectangleAnchor)
     */
    public void setShapeAnchor(RectangleAnchor anchor) {
        if (anchor == null) {
            throw new IllegalArgumentException("Null 'anchor' argument.");
        }
        this.shapeAnchor = anchor;    
    }
    
    /**
     * Returns the shape location.
     * 
     * @return The shape location.
     * 
     * @see #setShapeLocation(RectangleAnchor)
     */
    public RectangleAnchor getShapeLocation() {
        return this.shapeLocation;
    }
    
    /**
     * Sets the shape location.  This defines a point within the drawing
     * area that will be used to align the shape to.
     * 
     * @param location  the location (<code>null</code> not permitted).
     * 
     * @see #getShapeLocation()
     */
    public void setShapeLocation(RectangleAnchor location) {
        if (location == null) {
            throw new IllegalArgumentException("Null 'location' argument.");
        }
        this.shapeLocation = location;
    }
    
    /**
     * Returns the flag that controls whether or not the line is visible.
     * 
     * @return A boolean.
     * 
     * @see #setLineVisible(boolean)
     */
    public boolean isLineVisible() {
        return this.lineVisible;
    }
    
    /**
     * Sets the flag that controls whether or not the line is visible.
     * 
     * @param visible  the flag.
     * 
     * @see #isLineVisible()
     */
    public void setLineVisible(boolean visible) {
        this.lineVisible = visible;
    }

    /**
     * Returns the line centered about (0, 0).
     * 
     * @return The line.
     * 
     * @see #setLine(Shape)
     */
    public Shape getLine() {
        return this.line;
    }
    
    /**
     * Sets the line.  A Shape is used here, because then you can use Line2D, 
     * GeneralPath or any other Shape to represent the line.
     * 
     * @param line  the line.
     * 
     * @see #getLine()
     */
    public void setLine(Shape line) {
        this.line = line;
    }
    
    /**
     * Returns the line paint.
     * 
     * @return The paint.
     * 
     * @see #setLinePaint(Paint)
     */
    public Paint getLinePaint() {
        return this.linePaint;
    }
    
    /**
     * Sets the line paint.
     * 
     * @param paint  the paint.
     * 
     * @see #getLinePaint()
     */
    public void setLinePaint(Paint paint) {
        this.linePaint = paint;
    }
    
    /**
     * Returns the line stroke.
     * 
     * @return The stroke.
     * 
     * @see #setLineStroke(Stroke)
     */
    public Stroke getLineStroke() {
        return this.lineStroke;
    }
    
    /**
     * Sets the line stroke.
     * 
     * @param stroke  the stroke.
     * 
     * @see #getLineStroke()
     */
    public void setLineStroke(Stroke stroke) {
        this.lineStroke = stroke;
    }
    
    /**
     * Arranges the contents of the block, within the given constraints, and 
     * returns the block size.
     * 
     * @param g2  the graphics device.
     * @param constraint  the constraint (<code>null</code> not permitted).
     * 
     * @return The block size (in Java2D units, never <code>null</code>).
     */
    public Size2D arrange(Graphics2D g2, RectangleConstraint constraint) {
        RectangleConstraint contentConstraint = toContentConstraint(constraint);
        LengthConstraintType w = contentConstraint.getWidthConstraintType();
        LengthConstraintType h = contentConstraint.getHeightConstraintType();
        Size2D contentSize = null;
        if (w == LengthConstraintType.NONE) {
            if (h == LengthConstraintType.NONE) {
                contentSize = arrangeNN(g2);
            }
            else if (h == LengthConstraintType.RANGE) {
                throw new RuntimeException("Not yet implemented.");
            }
            else if (h == LengthConstraintType.FIXED) {
                throw new RuntimeException("Not yet implemented.");
            }
        }
        else if (w == LengthConstraintType.RANGE) {
            if (h == LengthConstraintType.NONE) {
                throw new RuntimeException("Not yet implemented.");
            }
            else if (h == LengthConstraintType.RANGE) {
                throw new RuntimeException("Not yet implemented.");
            }
            else if (h == LengthConstraintType.FIXED) {
                throw new RuntimeException("Not yet implemented.");
            }
        }
        else if (w == LengthConstraintType.FIXED) {
            if (h == LengthConstraintType.NONE) {
                throw new RuntimeException("Not yet implemented.");
            }
            else if (h == LengthConstraintType.RANGE) {
                throw new RuntimeException("Not yet implemented.");
            }
            else if (h == LengthConstraintType.FIXED) {   
                contentSize = new Size2D(
                    contentConstraint.getWidth(),
                    contentConstraint.getHeight()
                );
            }            
        }
        return new Size2D(
            calculateTotalWidth(contentSize.getWidth()), 
            calculateTotalHeight(contentSize.getHeight())
        );
    }
    
    /**
     * Performs the layout with no constraint, so the content size is 
     * determined by the bounds of the shape and/or line drawn to represent 
     * the series.
     * 
     * @param g2  the graphics device.
     * 
     * @return  The content size.
     */
    protected Size2D arrangeNN(Graphics2D g2) {
        Rectangle2D contentSize = new Rectangle2D.Double();
        if (this.line != null) {
            contentSize.setRect(this.line.getBounds2D());
        }
        if (this.shape != null) {
            contentSize = contentSize.createUnion(this.shape.getBounds2D());
        }
        return new Size2D(contentSize.getWidth(), contentSize.getHeight());
    }

    /**
     * Draws the graphic item within the specified area.
     * 
     * @param g2  the graphics device.
     * @param area  the area.
     */
    public void draw(Graphics2D g2, Rectangle2D area) {
        
        area = trimMargin(area);
        drawBorder(g2, area);
        area = trimBorder(area);
        area = trimPadding(area);
        
        if (this.lineVisible) {
            Point2D location = RectangleAnchor.coordinates(area, 
                    this.shapeLocation);
            Shape aLine = ShapeUtilities.createTranslatedShape(getLine(), 
                    this.shapeAnchor, location.getX(), location.getY());
            g2.setPaint(this.linePaint);
            g2.setStroke(this.lineStroke);
            g2.draw(aLine);
        }
        
        if (this.shapeVisible) {
            Point2D location = RectangleAnchor.coordinates(area, 
                    this.shapeLocation);
            
            Shape s = ShapeUtilities.createTranslatedShape(this.shape, 
                    this.shapeAnchor, location.getX(), location.getY());
            if (this.shapeFilled) {
                Paint p = this.fillPaint;
                if (p instanceof GradientPaint) {
                    GradientPaint gp = (GradientPaint) this.fillPaint;
                    p = this.fillPaintTransformer.transform(gp, s);
                }
                g2.setPaint(p);
                g2.fill(s);
            }
            if (this.shapeOutlineVisible) {
                g2.setPaint(this.outlinePaint);
                g2.setStroke(this.outlineStroke);
                g2.draw(s);
            }
        }
        
    }
    
    /**
     * Draws the block within the specified area.
     * 
     * @param g2  the graphics device.
     * @param area  the area.
     * @param params  ignored (<code>null</code> permitted).
     * 
     * @return Always <code>null</code>.
     */
    public Object draw(Graphics2D g2, Rectangle2D area, Object params) {
        draw(g2, area);
        return null;
    }
    
    /**
     * Tests this <code>LegendGraphic</code> instance for equality with an
     * arbitrary object.
     * 
     * @param obj  the object (<code>null</code> permitted).
     * 
     * @return A boolean.
     */
    public boolean equals(Object obj) {
        if (!(obj instanceof LegendGraphic)) {
            return false;
        }
        LegendGraphic that = (LegendGraphic) obj;
        if (this.shapeVisible != that.shapeVisible) {
            return false;
        }
        if (!ShapeUtilities.equal(this.shape, that.shape)) {
            return false;
        }
        if (this.shapeFilled != that.shapeFilled) {
            return false;
        }
        if (!PaintUtilities.equal(this.fillPaint, that.fillPaint)) {
            return false;
        }
        if (!ObjectUtilities.equal(this.fillPaintTransformer, 
                that.fillPaintTransformer)) {
            return false;
        }
        if (this.shapeOutlineVisible != that.shapeOutlineVisible) {
            return false;
        }
        if (!PaintUtilities.equal(this.outlinePaint, that.outlinePaint)) {
            return false;
        }
        if (!ObjectUtilities.equal(this.outlineStroke, that.outlineStroke)) {
            return false;
        }
        if (this.shapeAnchor != that.shapeAnchor) {
            return false;
        }
        if (this.shapeLocation != that.shapeLocation) {
            return false;
        }
        if (this.lineVisible != that.lineVisible) {
            return false;
        }
        if (!ShapeUtilities.equal(this.line, that.line)) {
            return false;
        }
        if (!PaintUtilities.equal(this.linePaint, that.linePaint)) {
            return false;
        }
        if (!ObjectUtilities.equal(this.lineStroke, that.lineStroke)) {
            return false;
        }
        return super.equals(obj);    
    }
    
    /**
     * Returns a hash code for this instance.
     * 
     * @return A hash code.
     */
    public int hashCode() {
        int result = 193;   
        result = 37 * result + ObjectUtilities.hashCode(this.fillPaint);
        // FIXME: use other fields too
        return result;
    }
    
    /**
     * Returns a clone of this <code>LegendGraphic</code> instance.
     * 
     * @return A clone of this <code>LegendGraphic</code> instance.
     * 
     * @throws CloneNotSupportedException if there is a problem cloning.
     */
    public Object clone() throws CloneNotSupportedException {
        LegendGraphic clone = (LegendGraphic) super.clone();
        clone.shape = ShapeUtilities.clone(this.shape);
        clone.line = ShapeUtilities.clone(this.line);
        return clone;
    }
    
    /**
     * 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();
        SerialUtilities.writeShape(this.shape, stream);
        SerialUtilities.writePaint(this.fillPaint, stream);
        SerialUtilities.writePaint(this.outlinePaint, stream);
        SerialUtilities.writeStroke(this.outlineStroke, stream);
        SerialUtilities.writeShape(this.line, stream);
        SerialUtilities.writePaint(this.linePaint, stream);
        SerialUtilities.writeStroke(this.lineStroke, 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.shape = SerialUtilities.readShape(stream);
        this.fillPaint = SerialUtilities.readPaint(stream);
        this.outlinePaint = SerialUtilities.readPaint(stream);
        this.outlineStroke = SerialUtilities.readStroke(stream);
        this.line = SerialUtilities.readShape(stream);
        this.linePaint = SerialUtilities.readPaint(stream);
        this.lineStroke = SerialUtilities.readStroke(stream);
    }

}
