/* ===========================================================

 * JFreeChart : a free chart library for the Java(tm) platform

 * ===========================================================

 *

 * (C) Copyright 2000-2004, 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., 59 Temple Place, Suite 330,

 * Boston, MA 02111-1307, USA.

 *

 * [Java is a trademark or registered trademark of Sun Microsystems, Inc.

 * in the United States and other countries.]

 *

 * ---------------

 * PeriodAxis.java

 * ---------------

 * (C) Copyright 2004, by Object Refinery Limited and Contributors.

 *

 * Original Author:  David Gilbert (for Object Refinery Limited);

 * Contributor(s):   -;

 *

 * $Id: PeriodAxis.java,v 1.1 2004/08/31 14:30:24 mungady Exp $

 *

 * Changes

 * -------

 * 01-Jun-2004 : Version 1 (DG);

 *

 */



package org.jfree.chart.axis;



import java.awt.FontMetrics;

import java.awt.Graphics2D;

import java.awt.geom.Line2D;

import java.awt.geom.Rectangle2D;

import java.io.Serializable;

import java.lang.reflect.Constructor;

import java.text.DateFormat;

import java.util.Arrays;

import java.util.Date;

import java.util.List;

import java.util.TimeZone;



import org.jfree.chart.event.AxisChangeEvent;

import org.jfree.chart.plot.Plot;

import org.jfree.chart.plot.PlotRenderingInfo;

import org.jfree.chart.plot.ValueAxisPlot;

import org.jfree.data.Range;

import org.jfree.data.time.Day;

import org.jfree.data.time.Month;

import org.jfree.data.time.RegularTimePeriod;

import org.jfree.data.time.Year;

import org.jfree.text.TextUtilities;

import org.jfree.ui.RectangleEdge;

import org.jfree.ui.TextAnchor;

import org.jfree.util.Log;

import org.jfree.util.LogContext;



/**

 * An axis that displays a date scale based on a {@link org.jfree.data.time.RegularTimePeriod}.

 */

public class PeriodAxis extends ValueAxis implements Cloneable, Serializable {

    

    /** The first time period in the overall range. */

    private RegularTimePeriod first;

    

    /** The last time period in the overall range. */

    private RegularTimePeriod last;

    

    /** The time zone used to convert 'first' and 'last' to absolute milliseconds. */

    private TimeZone timeZone;

    

    /** The {@link RegularTimePeriod} subclass used to automatically determine the axis range. */

    private Class autoRangeTimePeriodClass;

    

    /** Info for each labelling band. */

    private PeriodAxisLabelInfo[] labelInfo;

    

    /** Access to logging facilities. */

    private static final LogContext LOGGER = Log.createContext(PeriodAxis.class);



    /**

     * Creates a new axis.

     * 

     * @param label  the axis label.

     */

    public PeriodAxis(String label) {

        this(label, new Day(), new Day());

    }

    

    /**

     * Creates a new axis.

     * 

     * @param label  the axis label (<code>null</code> permitted).

     * @param first  the first time period in the axis range (<code>null</code> not permitted).

     * @param last  the last time period in the axis range (<code>null</code> not permitted).

     */

    public PeriodAxis(String label, RegularTimePeriod first, RegularTimePeriod last) {

        this(label, first, last, TimeZone.getDefault());

    }

    

    /**

     * Creates a new axis.

     * 

     * @param label  the axis label (<code>null</code> permitted).

     * @param first  the first time period in the axis range (<code>null</code> not permitted).

     * @param last  the last time period in the axis range (<code>null</code> not permitted).

     * @param timeZone  the time zone (<code>null</code> not permitted).

     */

    public PeriodAxis(String label, 

                      RegularTimePeriod first, RegularTimePeriod last, TimeZone timeZone) {

        super(label, null);

        this.first = first;

        this.last = last;

        this.timeZone = timeZone;

        this.autoRangeTimePeriodClass = Day.class;

        setAutoRange(true);

        this.labelInfo = new PeriodAxisLabelInfo[2];

        this.labelInfo[0] = new PeriodAxisLabelInfo(Month.class, "MMM");

        this.labelInfo[1] = new PeriodAxisLabelInfo(Year.class, "yyyy");

        

    }

    

    /**

     * Returns the first time period in the axis range.

     * 

     * @return The first time period (never <code>null</code>).

     */

    public RegularTimePeriod getFirst() {

        return this.first;

    }

    

    /**

     * Sets the first time period in the axis range and sends an {@link AxisChangeEvent} to

     * all registered listeners.

     * 

     * @param first  the time period (<code>null</code> not permitted).

     */

    public void setFirst(RegularTimePeriod first) {

        if (first == null) {

            throw new IllegalArgumentException("Null 'first' argument.");   

        }

        this.first = first;   

        notifyListeners(new AxisChangeEvent(this));

    }

    

    /**

     * Returns the last time period in the axis range.

     * 

     * @return The last time period (never <code>null</code>).

     */

    public RegularTimePeriod getLast() {

        return this.last;

    }

    

    /**

     * Sets the last time period in the axis range and sends an {@link AxisChangeEvent} to

     * all registered listeners.

     * 

     * @param last  the time period (<code>null</code> not permitted).

     */

    public void setLast(RegularTimePeriod last) {

        if (last == null) {

            throw new IllegalArgumentException("Null 'last' argument.");   

        }

        this.last = last;   

        notifyListeners(new AxisChangeEvent(this));

    }

    

    /**

     * Returns the time zone used to convert the periods defining the axis range into absolute

     * milliseconds.

     * 

     * @return The time zone (never <code>null</code>).

     */

    public TimeZone getTimeZone() {

        return this.timeZone;   

    }

    

    /**

     * Sets the time zone that is used to convert the time periods into absolute milliseconds.

     * 

     * @param zone  the time zone (<code>null</code> not permitted).

     */

    public void setTimeZone(TimeZone zone) {

        if (zone == null) {

            throw new IllegalArgumentException("Null 'zone' argument.");   

        }

        this.timeZone = zone;

        notifyListeners(new AxisChangeEvent(this));

    }

    

    /**

     * Returns the class used to create the first and last time periods for the axis range

     * when the auto-range flag is set to <code>true</code>.

     * 

     * @return The class (never <code>null</code>).

     */

    public Class getAutoRangeTimePeriodClass() {

        return this.autoRangeTimePeriodClass;   

    }

    

    /**

     * Sets the class used to create the first and last time periods for the axis range

     * when the auto-range flag is set to <code>true</code> and sends an {@link AxisChangeEvent} 

     * to all registered listeners.

     * 

     * @param c  the class (<code>null</code> not permitted).

     */

    public void setAutoRangeTimePeriodClass(Class c) {

        if (c == null) {

            throw new IllegalArgumentException("Null 'c' argument.");   

        }

        this.autoRangeTimePeriodClass = c;   

        notifyListeners(new AxisChangeEvent(this));

    }

    

    /**

     * Returns an array of label info records.

     * 

     * @return An array.

     */

    public PeriodAxisLabelInfo[] getLabelInfo() {

        return this.labelInfo;    

    }

    

    /**

     * Sets the array of label info records.

     * 

     * @param info  the info.

     */

    public void setLabelInfo(PeriodAxisLabelInfo[] info) {

        this.labelInfo = info;   

    }

    

    /**

     * Returns the range for the axis.

     *

     * @return The axis range (never <code>null</code>).

     */

    public Range getRange() {

        // TODO: find a cleaner way to do this...

        return new Range(

            this.first.getFirstMillisecond(this.timeZone), 

            this.last.getLastMillisecond(this.timeZone)

        );

    }



    /**

     * Configures the axis to work with the current plot.  Override this method

     * to perform any special processing (such as auto-rescaling).

     */

    public void configure() {

        autoAdjustRange();

    }



    /**

     * Estimates the space (height or width) required to draw the axis.

     *

     * @param g2  the graphics device.

     * @param plot  the plot that the axis belongs to.

     * @param plotArea  the area within which the plot (including axes) should be drawn.

     * @param edge  the axis location.

     * @param space  space already reserved.

     *

     * @return The space required to draw the axis (including pre-reserved space).

     */

    public AxisSpace reserveSpace(Graphics2D g2, Plot plot, 

                                  Rectangle2D plotArea, RectangleEdge edge, 

                                  AxisSpace space) {

        // create a new space object if one wasn't supplied...

        if (space == null) {

            space = new AxisSpace();

        }

        

        // if the axis is not visible, no additional space is required...

        if (!isVisible()) {

            return space;

        }



        // if the axis has a fixed dimension, return it...

        double dimension = getFixedDimension();

        if (dimension > 0.0) {

            space.ensureAtLeast(dimension, edge);

        }

        

        // get the axis label size and update the space object...

        Rectangle2D labelEnclosure = getLabelEnclosure(g2, edge);

        double labelHeight = 0.0;

        double labelWidth = 0.0;

        double tickLabelBandsDimension = 0.0;

        

        for (int i = 0; i < this.labelInfo.length; i++) {

            PeriodAxisLabelInfo info = this.labelInfo[i];

            FontMetrics fm = g2.getFontMetrics(info.getLabelFont());

            tickLabelBandsDimension += info.getSpacer().getAdjustedHeight(fm.getHeight());

        }

        

        if (RectangleEdge.isTopOrBottom(edge)) {

            labelHeight = labelEnclosure.getHeight();

            space.add(labelHeight + tickLabelBandsDimension, edge);

        }

        else if (RectangleEdge.isLeftOrRight(edge)) {

            labelWidth = labelEnclosure.getWidth();

            space.add(labelWidth + tickLabelBandsDimension, edge);

        }



        return space;

    }



    /**

     * Draws the axis on a Java 2D graphics device (such as the screen or a printer).

     *

     * @param g2  the graphics device (<code>null</code> not permitted).

     * @param cursor  the cursor location (determines where to draw the axis).

     * @param plotArea  the area within which the axes and plot should be drawn.

     * @param dataArea  the area within which the data should be drawn.

     * @param edge  the axis location (<code>null</code> not permitted).

     * @param plotState  collects information about the plot (<code>null</code> permitted).

     * 

     * @return The axis state (never <code>null</code>).

     */

    public AxisState draw(Graphics2D g2, 

                          double cursor,

                          Rectangle2D plotArea, 

                          Rectangle2D dataArea,

                          RectangleEdge edge,

                          PlotRenderingInfo plotState) {

        

        LOGGER.debug("In draw() method (cursor = " + cursor + ")");

        AxisState axisState = new AxisState(cursor);

        drawAxisLine(g2, cursor, dataArea, edge);

        for (int band = 0; band < this.labelInfo.length; band++) {

            axisState = drawTickLabels(band, g2, axisState, dataArea, edge);

        }

        

        // draw the axis label (note that 'state' is passed in *and* returned)...

        axisState = drawLabel(getLabel(), g2, plotArea, dataArea, edge, axisState);

        return axisState;

        

    }

    

    /**

     * Draws the tick labels for one "band" of time periods.

     * 

     * @param band  the band index (zero-based).

     * @param g2  the graphics device.

     * @param state  the axis state.

     * @param dataArea  the data area.

     * @param edge  the edge where the axis is located.

     * 

     * @return The updated axis state.

     */

    protected AxisState drawTickLabels(int band, Graphics2D g2, AxisState state, 

                                       Rectangle2D dataArea, RectangleEdge edge) {



        // work out the initial gap

        double delta1 = 0.0;

        FontMetrics fm = g2.getFontMetrics(this.labelInfo[band].getLabelFont());

        if (edge == RectangleEdge.BOTTOM) {

            delta1 = this.labelInfo[band].getSpacer().getTopSpace(fm.getHeight());   

        }

        else if (edge == RectangleEdge.TOP) {

            delta1 = this.labelInfo[band].getSpacer().getBottomSpace(fm.getHeight());   

        }

        state.moveCursor(delta1, edge);

        long axisMin = this.first.getFirstMillisecond(this.timeZone);

        long axisMax = this.last.getLastMillisecond(this.timeZone);

        g2.setFont(this.labelInfo[band].getLabelFont());

        g2.setPaint(this.labelInfo[band].getLabelPaint());



        // work out the number of periods to skip for labelling

        RegularTimePeriod p1 = this.labelInfo[band].createInstance(

            new Date(axisMin), this.timeZone

        );

        RegularTimePeriod p2 = this.labelInfo[band].createInstance(

            new Date(axisMax), this.timeZone

        );

        String label1 = this.labelInfo[band].getDateFormat().format(

            new Date(p1.getMiddleMillisecond(this.timeZone))

        );

        String label2 = this.labelInfo[band].getDateFormat().format(

            new Date(p2.getMiddleMillisecond(this.timeZone))

        );

        Rectangle2D b1 = TextUtilities.getTextBounds(label1, g2, g2.getFontMetrics());

        Rectangle2D b2 = TextUtilities.getTextBounds(label2, g2, g2.getFontMetrics());

        double w = Math.max(b1.getWidth(), b2.getWidth());

        long ww = Math.round(java2DToValue(dataArea.getX() + w + 5.0, dataArea, edge)) - axisMin;

        long length = p1.getLastMillisecond(this.timeZone) - p1.getFirstMillisecond(this.timeZone);

        int periods = (int) (ww / length) + 1;

        

        RegularTimePeriod p = this.labelInfo[band].createInstance(new Date(axisMin), this.timeZone);

        Rectangle2D b = null;

        long lastXX = 0L;

        float y = (float) (state.getCursor());

        TextAnchor anchor = TextAnchor.TOP_CENTER;

        if (edge == RectangleEdge.TOP) {

            anchor = TextAnchor.BOTTOM_CENTER;   

        }

        while (p.getFirstMillisecond(this.timeZone) <= axisMax) {

            float x = (float) valueToJava2D(p.getMiddleMillisecond(this.timeZone), dataArea, edge);

            DateFormat df = this.labelInfo[band].getDateFormat();

            String label = df.format(new Date(p.getMiddleMillisecond(this.timeZone)));

            long first = p.getFirstMillisecond(this.timeZone);

            long last = p.getLastMillisecond(this.timeZone);

            if (last > axisMax) {

                // this is the last period, but it is only partially visible so check that the

                // label will fit before displaying it...

                Rectangle2D bb = TextUtilities.getTextBounds(label, g2, g2.getFontMetrics());

                if ((x + bb.getWidth() / 2) > dataArea.getMaxX()) {

                    float xstart = (float) valueToJava2D(Math.max(first, axisMin), dataArea, edge);

                    if (bb.getWidth() < (dataArea.getMaxX() - xstart)) {

                        x = ((float) dataArea.getMaxX() + xstart) / 2.0f;   

                    }

                    else {

                        label = null;

                    }

                }

            }

            if (first < axisMin) {

                // this is the first period, but it is only partially visible so check that the

                // label will fit before displaying it...

                Rectangle2D bb = TextUtilities.getTextBounds(label, g2, g2.getFontMetrics());

                if ((x - bb.getWidth() / 2) < dataArea.getX()) {

                    float xlast = (float) valueToJava2D(Math.min(last, axisMax), dataArea, edge);

                    if (bb.getWidth() < (xlast - dataArea.getX())) {

                        x = (xlast + (float) dataArea.getX()) / 2.0f;   

                    }

                    else {

                        label = null;

                    }

                }

                

            }

            if (label != null) {

                g2.setPaint(this.labelInfo[band].getLabelPaint());

                b = TextUtilities.drawAlignedString(label, g2, x, y, anchor);

            }

            if (lastXX > 0L) {

                if (this.labelInfo[band].getDrawDividers()) {

                    long nextXX = p.getFirstMillisecond(this.timeZone);

                    long mid = (lastXX + nextXX) / 2;

                    float mid2d = (float) valueToJava2D(mid, dataArea, edge);

                    g2.setStroke(this.labelInfo[band].getDividerStroke());

                    g2.setPaint(this.labelInfo[band].getDividerPaint());

                    g2.draw(new Line2D.Float(mid2d, y, mid2d, y + (float) b1.getHeight()));

                }

            }

            lastXX = last;

            for (int i = 0; i < periods; i++) {

                p = p.next();   

            }

        }

        double used = 0.0;

        if (b != null) {

            used = b.getHeight();

            // work out the trailing gap

            if (edge == RectangleEdge.BOTTOM) {

                used += this.labelInfo[band].getSpacer().getBottomSpace(fm.getHeight());   

            }

            else if (edge == RectangleEdge.TOP) {

                used += this.labelInfo[band].getSpacer().getTopSpace(fm.getHeight());   

            }

        }

        state.moveCursor(used, edge);        

        return state;    

    }



    /**

     * Calculates the positions of the ticks for the axis, storing the results

     * in the tick list (ready for drawing).

     *

     * @param g2  the graphics device.

     * @param state  the axis state.

     * @param plotArea  the area within which the axes and plot should be drawn.

     * @param dataArea  the area inside the axes.

     * @param edge  the edge on which the axis is located.

     * 

     * @return The list of ticks.

     */

    public List refreshTicks(Graphics2D g2, 

                             AxisState state,

                             Rectangle2D plotArea,

                             Rectangle2D dataArea,

                             RectangleEdge edge) {

        return new java.util.ArrayList();

    }

    

    /**

     * Converts a data value to a coordinate in Java2D space, assuming that the

     * axis runs along one edge of the specified dataArea.

     * <p>

     * Note that it is possible for the coordinate to fall outside the area.

     *

     * @param value  the data value.

     * @param area  the area for plotting the data.

     * @param edge  the edge along which the axis lies.

     *

     * @return The Java2D coordinate.

     */

    public double valueToJava2D(double value,

                                Rectangle2D area,

                                RectangleEdge edge) {

        

        double result = Double.NaN;

        double axisMin = this.first.getFirstMillisecond(this.timeZone);

        double axisMax = this.last.getLastMillisecond(this.timeZone);

        if (RectangleEdge.isTopOrBottom(edge)) {

            double minX = area.getX();

            double maxX = area.getMaxX();

            if (isInverted()) {

                result = maxX + ((value - axisMin) / (axisMax - axisMin)) * (minX - maxX);

            }

            else {

                result = minX + ((value - axisMin) / (axisMax - axisMin)) * (maxX - minX);

            }

        }

        else if (RectangleEdge.isLeftOrRight(edge)) {

            double minY = area.getMinY();

            double maxY = area.getMaxY();

            if (isInverted()) {

                result = minY + (((value - axisMin) / (axisMax - axisMin)) * (maxY - minY));

            }

            else {

                result = maxY - (((value - axisMin) / (axisMax - axisMin)) * (maxY - minY));

            }

        }

        return result;

        

    }



    /**

     * Converts a coordinate in Java2D space to the corresponding data value,

     * assuming that the axis runs along one edge of the specified dataArea.

     *

     * @param java2DValue  the coordinate in Java2D space.

     * @param area  the area in which the data is plotted.

     * @param edge  the edge along which the axis lies.

     *

     * @return the data value.

     */

    public double java2DToValue(double java2DValue,

                                Rectangle2D area,

                                RectangleEdge edge) {



        double result = Double.NaN;

        double min = 0.0;

        double max = 0.0;

        double axisMin = this.first.getFirstMillisecond(this.timeZone);

        double axisMax = this.last.getLastMillisecond(this.timeZone);

        if (RectangleEdge.isTopOrBottom(edge)) {

            min = area.getX();

            max = area.getMaxX();

        }

        else if (RectangleEdge.isLeftOrRight(edge)) {

            min = area.getMaxY();

            max = area.getY();

        }

        if (isInverted()) {

             result = axisMax - ((java2DValue - min) / (max - min) * (axisMax - axisMin));

        }

        else {

             result = axisMin + ((java2DValue - min) / (max - min) * (axisMax - axisMin));

        }

        return result;

    }



    /**

     * Rescales the axis to ensure that all data is visible.

     */

    protected void autoAdjustRange() {



        Plot plot = getPlot();

        if (plot == null) {

            return;  // no plot, no data

        }



        if (plot instanceof ValueAxisPlot) {

            ValueAxisPlot vap = (ValueAxisPlot) plot;



            Range r = vap.getDataRange(this);

            if (r == null) {

                r = new Range(DEFAULT_LOWER_BOUND, DEFAULT_UPPER_BOUND);

            }

            

            long upper = Math.round(r.getUpperBound());

            long lower = Math.round(r.getLowerBound());

            this.first = createInstance(

                this.autoRangeTimePeriodClass, new Date(lower), this.timeZone

            );

            this.last = createInstance(

                this.autoRangeTimePeriodClass, new Date(upper), this.timeZone

            );

        }



    }

    

    /**

     * Tests the axis for equality with an arbitrary object.

     * 

     * @param obj  the object (<code>null</code> permitted).

     * 

     * @return A boolean.

     */

    public boolean equals(Object obj) {

        if (obj == this) {

            return true;   

        }

        if (obj instanceof PeriodAxis) {

            PeriodAxis a = (PeriodAxis) obj;

            if (!this.first.equals(a.first)) {

                return false;   

            }

            if (!this.last.equals(a.last)) {

                return false;   

            }

            if (!this.timeZone.equals(a.timeZone)) {

                return false;   

            }

            if (!this.autoRangeTimePeriodClass.equals(a.autoRangeTimePeriodClass)) {

                return false;   

            }

            if (!Arrays.equals(this.labelInfo, a.labelInfo)) {

                return false;   

            }

            return true;   

        }

        return false;

    }

    

    /**

     * A utility method used to create a particular subclass of the {@link RegularTimePeriod} class

     * that includes the specified millisecond, assuming the specified time zone.

     * 

     * @param periodClass  the class.

     * @param millisecond  the time.

     * @param zone  the time zone.

     * 

     * @return The time period.

     */

    private RegularTimePeriod createInstance(Class periodClass, Date millisecond, TimeZone zone) {

        RegularTimePeriod result = null;

        try {

            Constructor c = periodClass.getDeclaredConstructor(

                new Class[] {Date.class, TimeZone.class}

            );

            result = (RegularTimePeriod) c.newInstance(new Object[] {millisecond, zone});   

        }

        catch (Exception e) {

            // do nothing            

        }

        return result;

    }



}

