/* ===========================================================
 * JFreeChart : a free chart library for the Java(tm) platform
 * ===========================================================
 *
 * (C) Copyright 2000-2012, 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.
 *
 * [Oracle and Java are registered trademarks of Oracle and/or its affiliates.
 * Other names may be trademarks of their respective owners.]
 *
 * ------------------
 * AbstractBlock.java
 * ------------------
 * (C) Copyright 2004-2012, by Object Refinery Limited.
 *
 * Original Author:  David Gilbert (for Object Refinery Limited);
 * Contributor(s):   -;
 *
 * Changes:
 * --------
 * 22-Oct-2004 : Version 1 (DG);
 * 02-Feb-2005 : Added accessor methods for margin (DG);
 * 04-Feb-2005 : Added equals() method and implemented Serializable (DG);
 * 03-May-2005 : Added null argument checks (DG);
 * 06-May-2005 : Added convenience methods for setting margin, border and
 *               padding (DG);
 * ------------- JFREECHART 1.0.x ---------------------------------------------
 * 16-Mar-2007 : Changed border from BlockBorder to BlockFrame, updated
 *               equals(), and implemented Cloneable (DG);
 * 15-Jun-2012 : Removed JCommon dependencies (DG);
 *
 */

package org.jfree.chart.block;

import java.awt.Graphics2D;
import java.awt.geom.Rectangle2D;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;

import org.jfree.chart.ui.RectangleInsets;
import org.jfree.chart.ui.Size2D;
import org.jfree.chart.util.ObjectUtilities;
import org.jfree.chart.util.PublicCloneable;
import org.jfree.chart.util.ShapeUtilities;
import org.jfree.chart.util.SerialUtilities;
import org.jfree.data.Range;

/**
 * A convenience class for creating new classes that implement
 * the {@link Block} interface.
 */
public class AbstractBlock implements Cloneable, Serializable {

    /** For serialization. */
    private static final long serialVersionUID = 7689852412141274563L;

    /** The id for the block. */
    private String id;

    /** The margin around the outside of the block. */
    private RectangleInsets margin;

    /** The frame (or border) for the block. */
    private BlockFrame frame;

    /** The padding between the block content and the border. */
    private RectangleInsets padding;

    /**
     * The natural width of the block (may be overridden if there are
     * constraints in sizing).
     */
    private double width;

    /**
     * The natural height of the block (may be overridden if there are
     * constraints in sizing).
     */
    private double height;

    /**
     * The current bounds for the block (position of the block in Java2D space).
     */
    private transient Rectangle2D bounds;

    /**
     * Creates a new block.
     */
    protected AbstractBlock() {
        this.id = null;
        this.width = 0.0;
        this.height = 0.0;
        this.bounds = new Rectangle2D.Float();
        this.margin = RectangleInsets.ZERO_INSETS;
        this.frame = BlockBorder.NONE;
        this.padding = RectangleInsets.ZERO_INSETS;
    }

    /**
     * Returns the id.
     *
     * @return The id (possibly <code>null</code>).
     *
     * @see #setID(String)
     */
    public String getID() {
        return this.id;
    }

    /**
     * Sets the id for the block.
     *
     * @param id  the id (<code>null</code> permitted).
     *
     * @see #getID()
     */
    public void setID(String id) {
        this.id = id;
    }

    /**
     * Returns the natural width of the block, if this is known in advance.
     * The actual width of the block may be overridden if layout constraints
     * make this necessary.
     *
     * @return The width.
     *
     * @see #setWidth(double)
     */
    public double getWidth() {
        return this.width;
    }

    /**
     * Sets the natural width of the block, if this is known in advance.
     *
     * @param width  the width (in Java2D units)
     *
     * @see #getWidth()
     */
    public void setWidth(double width) {
        this.width = width;
    }

    /**
     * Returns the natural height of the block, if this is known in advance.
     * The actual height of the block may be overridden if layout constraints
     * make this necessary.
     *
     * @return The height.
     *
     * @see #setHeight(double)
     */
    public double getHeight() {
        return this.height;
    }

    /**
     * Sets the natural width of the block, if this is known in advance.
     *
     * @param height  the width (in Java2D units)
     *
     * @see #getHeight()
     */
    public void setHeight(double height) {
        this.height = height;
    }

    /**
     * Returns the margin.
     *
     * @return The margin (never <code>null</code>).
     *
     * @see #getMargin()
     */
    public RectangleInsets getMargin() {
        return this.margin;
    }

    /**
     * Sets the margin (use {@link RectangleInsets#ZERO_INSETS} for no
     * padding).
     *
     * @param margin  the margin (<code>null</code> not permitted).
     *
     * @see #getMargin()
     */
    public void setMargin(RectangleInsets margin) {
        if (margin == null) {
            throw new IllegalArgumentException("Null 'margin' argument.");
        }
        this.margin = margin;
    }

    /**
     * Sets the margin.
     *
     * @param top  the top margin.
     * @param left  the left margin.
     * @param bottom  the bottom margin.
     * @param right  the right margin.
     *
     * @see #getMargin()
     */
    public void setMargin(double top, double left, double bottom,
                          double right) {
        setMargin(new RectangleInsets(top, left, bottom, right));
    }

    /**
     * Sets a black border with the specified line widths.
     *
     * @param top  the top border line width.
     * @param left  the left border line width.
     * @param bottom  the bottom border line width.
     * @param right  the right border line width.
     */
    public void setBorder(double top, double left, double bottom,
                          double right) {
        setFrame(new BlockBorder(top, left, bottom, right));
    }

    /**
     * Returns the current frame (border).
     *
     * @return The frame.
     *
     * @since 1.0.5
     * @see #setFrame(BlockFrame)
     */
    public BlockFrame getFrame() {
        return this.frame;
    }

    /**
     * Sets the frame (or border).
     *
     * @param frame  the frame (<code>null</code> not permitted).
     *
     * @since 1.0.5
     * @see #getFrame()
     */
    public void setFrame(BlockFrame frame) {
        if (frame == null) {
            throw new IllegalArgumentException("Null 'frame' argument.");
        }
        this.frame = frame;
    }

    /**
     * Returns the padding.
     *
     * @return The padding (never <code>null</code>).
     *
     * @see #setPadding(RectangleInsets)
     */
    public RectangleInsets getPadding() {
        return this.padding;
    }

    /**
     * Sets the padding (use {@link RectangleInsets#ZERO_INSETS} for no
     * padding).
     *
     * @param padding  the padding (<code>null</code> not permitted).
     *
     * @see #getPadding()
     */
    public void setPadding(RectangleInsets padding) {
        if (padding == null) {
            throw new IllegalArgumentException("Null 'padding' argument.");
        }
        this.padding = padding;
    }

    /**
     * Sets the padding.
     *
     * @param top  the top padding.
     * @param left  the left padding.
     * @param bottom  the bottom padding.
     * @param right  the right padding.
     */
    public void setPadding(double top, double left, double bottom,
                           double right) {
        setPadding(new RectangleInsets(top, left, bottom, right));
    }

    /**
     * Returns the x-offset for the content within the block.
     *
     * @return The x-offset.
     *
     * @see #getContentYOffset()
     */
    public double getContentXOffset() {
        return this.margin.getLeft() + this.frame.getInsets().getLeft()
            + this.padding.getLeft();
    }

    /**
     * Returns the y-offset for the content within the block.
     *
     * @return The y-offset.
     *
     * @see #getContentXOffset()
     */
    public double getContentYOffset() {
        return this.margin.getTop() + this.frame.getInsets().getTop()
            + this.padding.getTop();
    }

    /**
     * Arranges the contents of the block, with no constraints, and returns
     * the block size.
     *
     * @param g2  the graphics device.
     *
     * @return The block size (in Java2D units, never <code>null</code>).
     */
    public Size2D arrange(Graphics2D g2) {
        return arrange(g2, RectangleConstraint.NONE);
    }

    /**
     * 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) {
        Size2D base = new Size2D(getWidth(), getHeight());
        return constraint.calculateConstrainedSize(base);
    }

    /**
     * Returns the current bounds of the block.
     *
     * @return The bounds.
     *
     * @see #setBounds(Rectangle2D)
     */
    public Rectangle2D getBounds() {
        return this.bounds;
    }

    /**
     * Sets the bounds of the block.
     *
     * @param bounds  the bounds (<code>null</code> not permitted).
     *
     * @see #getBounds()
     */
    public void setBounds(Rectangle2D bounds) {
        if (bounds == null) {
            throw new IllegalArgumentException("Null 'bounds' argument.");
        }
        this.bounds = bounds;
    }

    /**
     * Calculate the width available for content after subtracting
     * the margin, border and padding space from the specified fixed
     * width.
     *
     * @param fixedWidth  the fixed width.
     *
     * @return The available space.
     *
     * @see #trimToContentHeight(double)
     */
    protected double trimToContentWidth(double fixedWidth) {
        double result = this.margin.trimWidth(fixedWidth);
        result = this.frame.getInsets().trimWidth(result);
        result = this.padding.trimWidth(result);
        return Math.max(result, 0.0);
    }

    /**
     * Calculate the height available for content after subtracting
     * the margin, border and padding space from the specified fixed
     * height.
     *
     * @param fixedHeight  the fixed height.
     *
     * @return The available space.
     *
     * @see #trimToContentWidth(double)
     */
    protected double trimToContentHeight(double fixedHeight) {
        double result = this.margin.trimHeight(fixedHeight);
        result = this.frame.getInsets().trimHeight(result);
        result = this.padding.trimHeight(result);
        return Math.max(result, 0.0);
    }

    /**
     * Returns a constraint for the content of this block that will result in
     * the bounds of the block matching the specified constraint.
     *
     * @param c  the outer constraint (<code>null</code> not permitted).
     *
     * @return The content constraint.
     */
    protected RectangleConstraint toContentConstraint(RectangleConstraint c) {
        if (c == null) {
            throw new IllegalArgumentException("Null 'c' argument.");
        }
        if (c.equals(RectangleConstraint.NONE)) {
            return c;
        }
        double w = c.getWidth();
        Range wr = c.getWidthRange();
        double h = c.getHeight();
        Range hr = c.getHeightRange();
        double ww = trimToContentWidth(w);
        double hh = trimToContentHeight(h);
        Range wwr = trimToContentWidth(wr);
        Range hhr = trimToContentHeight(hr);
        return new RectangleConstraint(
            ww, wwr, c.getWidthConstraintType(),
            hh, hhr, c.getHeightConstraintType()
        );
    }

    private Range trimToContentWidth(Range r) {
        if (r == null) {
            return null;
        }
        double lowerBound = 0.0;
        double upperBound = Double.POSITIVE_INFINITY;
        if (r.getLowerBound() > 0.0) {
            lowerBound = trimToContentWidth(r.getLowerBound());
        }
        if (r.getUpperBound() < Double.POSITIVE_INFINITY) {
            upperBound = trimToContentWidth(r.getUpperBound());
        }
        return new Range(lowerBound, upperBound);
    }

    private Range trimToContentHeight(Range r) {
        if (r == null) {
            return null;
        }
        double lowerBound = 0.0;
        double upperBound = Double.POSITIVE_INFINITY;
        if (r.getLowerBound() > 0.0) {
            lowerBound = trimToContentHeight(r.getLowerBound());
        }
        if (r.getUpperBound() < Double.POSITIVE_INFINITY) {
            upperBound = trimToContentHeight(r.getUpperBound());
        }
        return new Range(lowerBound, upperBound);
    }

    /**
     * Adds the margin, border and padding to the specified content width.
     *
     * @param contentWidth  the content width.
     *
     * @return The adjusted width.
     */
    protected double calculateTotalWidth(double contentWidth) {
        double result = contentWidth;
        result = this.padding.extendWidth(result);
        result = this.frame.getInsets().extendWidth(result);
        result = this.margin.extendWidth(result);
        return result;
    }

    /**
     * Adds the margin, border and padding to the specified content height.
     *
     * @param contentHeight  the content height.
     *
     * @return The adjusted height.
     */
    protected double calculateTotalHeight(double contentHeight) {
        double result = contentHeight;
        result = this.padding.extendHeight(result);
        result = this.frame.getInsets().extendHeight(result);
        result = this.margin.extendHeight(result);
        return result;
    }

    /**
     * Reduces the specified area by the amount of space consumed
     * by the margin.
     *
     * @param area  the area (<code>null</code> not permitted).
     *
     * @return The trimmed area.
     */
    protected Rectangle2D trimMargin(Rectangle2D area) {
        // defer argument checking...
        this.margin.trim(area);
        return area;
    }

    /**
     * Reduces the specified area by the amount of space consumed
     * by the border.
     *
     * @param area  the area (<code>null</code> not permitted).
     *
     * @return The trimmed area.
     */
    protected Rectangle2D trimBorder(Rectangle2D area) {
        // defer argument checking...
        this.frame.getInsets().trim(area);
        return area;
    }

    /**
     * Reduces the specified area by the amount of space consumed
     * by the padding.
     *
     * @param area  the area (<code>null</code> not permitted).
     *
     * @return The trimmed area.
     */
    protected Rectangle2D trimPadding(Rectangle2D area) {
        // defer argument checking...
        this.padding.trim(area);
        return area;
    }

    /**
     * Draws the border around the perimeter of the specified area.
     *
     * @param g2  the graphics device.
     * @param area  the area.
     */
    protected void drawBorder(Graphics2D g2, Rectangle2D area) {
        this.frame.draw(g2, area);
    }

    /**
     * Tests this block for equality with an arbitrary object.
     *
     * @param obj  the object (<code>null</code> permitted).
     *
     * @return A boolean.
     */
    @Override
    public boolean equals(Object obj) {
        if (obj == this) {
            return true;
        }
        if (!(obj instanceof AbstractBlock)) {
            return false;
        }
        AbstractBlock that = (AbstractBlock) obj;
        if (!ObjectUtilities.equal(this.id, that.id)) {
            return false;
        }
        if (!this.frame.equals(that.frame)) {
            return false;
        }
        if (!this.bounds.equals(that.bounds)) {
            return false;
        }
        if (!this.margin.equals(that.margin)) {
            return false;
        }
        if (!this.padding.equals(that.padding)) {
            return false;
        }
        if (this.height != that.height) {
            return false;
        }
        if (this.width != that.width) {
            return false;
        }
        return true;
    }

    /**
     * Returns a clone of this block.
     *
     * @return A clone.
     *
     * @throws CloneNotSupportedException if there is a problem creating the
     *         clone.
     */
    @Override
    public Object clone() throws CloneNotSupportedException {
        AbstractBlock clone = (AbstractBlock) super.clone();
        clone.bounds = (Rectangle2D) ShapeUtilities.clone(this.bounds);
        if (this.frame instanceof PublicCloneable) {
            PublicCloneable pc = (PublicCloneable) this.frame;
            clone.frame = (BlockFrame) pc.clone();
        }
        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.bounds, 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.bounds = (Rectangle2D) SerialUtilities.readShape(stream);
    }

}
