/*
 * Redistribution and use of this software and associated documentation
 * ("Software"), with or without modification, are permitted provided that the
 * following conditions are met:
 *
 * 1. Redistributions of source code must retain copyright statements and
 * notices. Redistributions must also contain a copy of this document.
 *
 * 2. Redistributions in binary form must reproduce the above copyright notice,
 * this list of conditions and the following disclaimer in the documentation
 * and/or other materials provided with the distribution.
 *
 * 3. The name "Exolab" must not be used to endorse or promote products derived
 * from this Software without prior written permission of Intalio, Inc. For
 * written permission, please contact info@exolab.org.
 *
 * 4. Products derived from this Software may not be called "Exolab" nor may
 * "Exolab" appear in their names without prior written permission of Intalio,
 * Inc. Exolab is a registered trademark of Intalio, Inc.
 *
 * 5. Due credit should be given to the Exolab Project (http://www.exolab.org/).
 *
 * THIS SOFTWARE IS PROVIDED BY INTALIO, INC. AND CONTRIBUTORS ``AS IS'' AND ANY
 * EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL INTALIO, INC. OR ITS CONTRIBUTORS BE LIABLE FOR
 * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * Copyright 1999-2004 (C) Intalio, Inc. All Rights Reserved.
 *
 * $Id: DateFieldHandler.java 7306 2007-12-06 09:48:33Z wguttmn $
 */
package org.exolab.castor.xml.handlers;

import java.lang.reflect.Array;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.Enumeration;
import java.util.GregorianCalendar;
import java.util.TimeZone;
import java.util.Vector;

import org.exolab.castor.mapping.FieldHandler;
import org.exolab.castor.types.DateTime;
import org.exolab.castor.xml.XMLFieldHandler;

/**
 * A specialized FieldHandler for the XML Schema Date/Time related types.
 *
 * @author <a href="kvisco-at-intalio.com">Keith Visco</a>
 * @version $Revision: 7306 $ $Date: 2005-02-09 13:04:19 -0700 (Wed, 09 Feb
 *          2005) $
 */
public class DateFieldHandler extends XMLFieldHandler {

    /** The default length of the date string, used by the format method. */
    private static final byte         DEFAULT_DATE_LENGTH       = 25;
    /** The error message prefix. */
    private static final String       INVALID_DATE              = "Invalid dateTime format: ";
    /** The default parse options when none are specified. */
    private static final ParseOptions DEFAULT_PARSE_OPTIONS     = new ParseOptions();

    /** The local timezone offset from UTC. */
    private static TimeZone           _timezone                 = TimeZone.getDefault();
    /** A boolean to indicate that the TimeZone can be suppressed if the TimeZone
     * is equivalent to the "default" timezone. */
    private static boolean            _allowTimeZoneSuppression = false;
    /** if true, milliseconds should be suppressed upon formatting. */
    private static boolean            _suppressMillis           = false;

    /** The nested FieldHandler. */
    private final FieldHandler        _handler;
    /** The current set of parse options. */
    private ParseOptions              _options                  = new ParseOptions();
    /** A flag to indicate that java.sql.Date should be returned instead. */
    private boolean                   _useSQLDate               = false;

    // ----------------/
    // - Constructors -/
    // ----------------/

    /**
     * Creates a new DateFieldHandler using the given FieldHandler for
     * delegation.
     *
     * @param fieldHandler the fieldHandler for delegation.
     */
    public DateFieldHandler(final FieldHandler fieldHandler) {
        if (fieldHandler == null) {
            String err = "The FieldHandler argument passed to "
                    + "the constructor of DateFieldHandler must not be null.";
            throw new IllegalArgumentException(err);
        }
        _handler = fieldHandler;
    } // -- DateFieldHandler

    // ------------------/
    // - Public Methods -/
    // ------------------/

    /**
     * Returns the value of the field associated with this descriptor from the
     * given target object.
     *
     * @param target the object to get the value from
     * @return the value of the field associated with this descriptor from the
     *         given target object.
     */
    public Object getValue(final Object target) {
        Object val = _handler.getValue(target);
        if (val == null) {
            return val;
        }

        Object formatted = null;

        Class type = val.getClass();

        if (java.util.Date.class.isAssignableFrom(type)) {
            formatted = format((Date) val);
        } else if (type.isArray()) {
            int size = Array.getLength(val);
            String[] values = new String[size];
            for (int i = 0; i < size; i++) {
                values[i] = format(Array.get(val, i));
            }
            formatted = values;
        } else if (java.util.Enumeration.class.isAssignableFrom(type)) {
            Enumeration enumeration = (Enumeration) val;
            Vector values = new Vector();
            while (enumeration.hasMoreElements()) {
                values.addElement(format(enumeration.nextElement()));
            }
            String[] valuesArray = new String[values.size()];
            values.copyInto(valuesArray);
            formatted = valuesArray;
        } else {
            formatted = val.toString();
        }
        return formatted;
    } // -- getValue

    /**
     * Sets the value of the field associated with this descriptor.
     *
     * @param target the object in which to set the value
     * @param value the value of the field
     * @throws IllegalStateException if the value provided cannot be parsed into
     *         a legal date/time.
     */
    public void setValue(final Object target, final Object value)
                                        throws java.lang.IllegalStateException {
        Date date = null;

        if (value == null || value instanceof Date) {
            date = (Date) value;
        } else {
            try {
                date = parse(value.toString(), _options);
                // -- java.sql.Date?
                if (_useSQLDate && date != null) {
                    date = new java.sql.Date(date.getTime());
                }
            } catch (java.text.ParseException px) {
                // -- invalid dateTime
                throw new IllegalStateException(px.getMessage());
            }
        }

        _handler.setValue(target, date);
    } // -- setValue

    /**
     * Sets the value of the field to a default value.
     *
     * @param target The object
     * @throws IllegalStateException The Java object has changed and is no
     *         longer supported by this handler, or the handler is not
     *         compatiable with the Java object
     */
    public void resetValue(final Object target) throws java.lang.IllegalStateException {
        _handler.resetValue(target);
    }

    /**
     * Creates a new instance of the object described by this field.
     *
     * @param parent The object for which the field is created
     * @return A new instance of the field's value
     * @throws IllegalStateException This field is a simple type and cannot be
     *         instantiated
     */
    public Object newInstance(final Object parent) throws IllegalStateException {
        Object obj = _handler.newInstance(parent);
        if (obj == null) {
            obj = new Date();
        }
        return obj;
    } // -- newInstance

    /**
     * Returns true if the given object is an XMLFieldHandler that is equivalent
     * to the delegated handler. An equivalent XMLFieldHandler is an
     * XMLFieldHandler that is an instances of the same class.
     *
     * @param obj The object to compare against <code>this</code>
     * @return true if the given object is an XMLFieldHandler that is equivalent
     *         to this one.
     */
    public boolean equals(final Object obj) {
        if (obj == null) {
            return false;
        }
        if (obj == this) {
            return true;
        }
        if (!(obj instanceof FieldHandler)) {
            return false;
        }
        return (_handler.getClass().isInstance(obj) || getClass().isInstance(obj));
    } // -- equals

    /**
     * Sets whether or not the time zone should always be displayed when
     * marshaling xsd:dateTime values. If true, then the time zone will not be
     * displayed if the time zone is the current local time zone.
     *
     * @param allowTimeZoneSuppression if true, the time zone will not be
     *        displayed if it is the current local time zone.
     */
    public static void setAllowTimeZoneSuppression(final boolean allowTimeZoneSuppression) {
        _allowTimeZoneSuppression = allowTimeZoneSuppression;
    } // -- setAlwaysUseUTCTime

    /**
     * Sets the default TimeZone used for comparing dates when marshaling
     * xsd:dateTime values using this handler. This is used when determining if
     * the timezone can be omitted when marshaling.
     *
     * Default is JVM default returned by TimeZone.getDefault()
     *
     * @param timeZone TimeZone to use instead of JVM default
     * @see #setAllowTimeZoneSuppression
     */
    public static void setDefaultTimeZone(final TimeZone timeZone) {
        if (timeZone == null) {
            // -- reset timezone to the default
            _timezone = TimeZone.getDefault();
        } else {
            _timezone = (TimeZone) timeZone.clone();
        }
    } // -- setDefaultTimeZone

    /**
     * Sets a flag indicating whether or not Milliseconds should be suppressed
     * upon formatting a dateTime as a String.
     *
     * @param suppressMillis a boolean when true indicates that millis should be
     *        suppressed during conversion of a dateTime to a String
     */
    public static void setSuppressMillis(final boolean suppressMillis) {
        _suppressMillis = suppressMillis;
    } // -- setAlwaysUseUTCTime

    /**
     * Specifies that this DateFieldHandler should use java.sql.Date when
     * creating new Date instances.
     *
     * @param useSQLDate a boolean that when true indicates that java.sql.Date
     *        should be used instead of java.util.Date.
     */
    public void setUseSQLDate(final boolean useSQLDate) {
        _useSQLDate = useSQLDate;
        _options._allowNoTime = _useSQLDate;
    } // -- setUseSQLDate

    // -------------------/
    // - Private Methods -/
    // -------------------/

    /**
     * Parses the given string, which must be in the following format:
     * <b>CCYY-MM-DDThh:mm:ss</b> or <b>CCYY-MM-DDThh:mm:ss.sss</b> where "CC"
     * represents the century, "YY" the year, "MM" the month and "DD" the day.
     * The letter "T" is the date/time separator and "hh", "mm", "ss" represent
     * hour, minute and second respectively.
     * <p>
     * CCYY represents the Year and each 'C' and 'Y' must be a digit from 0-9. A
     * minimum of 4 digits must be present.
     * <p>
     * MM represents the month and each 'M' must be a digit from 0-9, but
     * together "MM" must not represent a value greater than 12. "MM" must be 2
     * digits, use of leading zero is required for all values less than 10.
     * <p>
     * DD represents the day of the month and each 'D' must be a digit from 0-9.
     * DD must be 2 digits (use a leading zero if necessary) and must not be
     * greater than 31.
     * <p>
     * 'T' is the date/time separator and must exist!
     * <p>
     * hh represents the hour using 0-23. mm represents the minute using 0-59.
     * ss represents the second using 0-60. (60 for leap second) sss represents
     * the millisecond using 0-999.
     *
     * @param dateTime the string to convert to a Date
     * @return a new Date that represents the given string.
     * @throws ParseException when the given string does not conform to the
     *            above string.
     */
    protected static Date parse(final String dateTime) throws ParseException {
        return parse(dateTime, DEFAULT_PARSE_OPTIONS);
    } // -- parse

    /**
     * Parses the given string, which must be in the following format:
     * <b>CCYY-MM-DDThh:mm:ss</b> or <b>CCYY-MM-DDThh:mm:ss.sss</b> where "CC"
     * represents the century, "YY" the year, "MM" the month and "DD" the day.
     * The letter "T" is the date/time separator and "hh", "mm", "ss" represent
     * hour, minute and second respectively.
     * <p>
     * CCYY represents the Year and each 'C' and 'Y' must be a digit from 0-9. A
     * minimum of 4 digits must be present.
     * <p>
     * MM represents the month and each 'M' must be a digit from 0-9, but
     * together "MM" must not represent a value greater than 12. "MM" must be 2
     * digits, use of leading zero is required for all values less than 10.
     * <p>
     * DD represents the day of the month and each 'D' must be a digit from 0-9.
     * DD must be 2 digits (use a leading zero if necessary) and must not be
     * greater than 31.
     * <p>
     * 'T' is the date/time separator and must exist!
     * <p>
     * hh represents the hour using 0-23. mm represents the minute using 0-59.
     * ss represents the second using 0-60. (60 for leap second) sss represents
     * the millisecond using 0-999.
     *
     * @param dateTime the string to convert to a Date
     * @param options the parsing options to use
     * @return a new Date that represents the given string.
     * @throws ParseException when the given string does not conform to the
     *            above string.
     */
    protected static Date parse(final String dateTime, final ParseOptions options)
                                                         throws ParseException {
        if (dateTime == null) {
            throw new ParseException(INVALID_DATE + "null", 0);
        }

        ParseOptions pOptions = (options != null) ? options : DEFAULT_PARSE_OPTIONS;

        String trimmed = dateTime.trim();

        // If no time is present and we don't require time, use org.exolab.castor.types.Date
        if (pOptions._allowNoTime && trimmed.indexOf('T') == -1) {
            org.exolab.castor.types.Date parsedDate = new org.exolab.castor.types.Date(trimmed);
            return parsedDate.toDate();
        }

        DateTime parsedDateTime = new DateTime(trimmed);
        return parsedDateTime.toDate();
    } // -- parse

    /**
     * Returns the given date in a String format, using the ISO8601 format as
     * specified in the W3C XML Schema 1.0 Recommendation (Part 2: Datatypes)
     * for dateTime.
     *
     * @param date the Date to format
     * @return the formatted string
     */
    protected static String format(final Date date) {
        final SimpleDateFormat formatter;
        if (_suppressMillis) {
            formatter = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss");
        } else {
            formatter = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS");
        }

        /* ensure the formatter does not use the default system timezone */
        formatter.setTimeZone(_timezone);
        
        GregorianCalendar cal = new GregorianCalendar();
        cal.setTime(date);
        cal.setTimeZone(_timezone);

        StringBuffer buffer = new StringBuffer(DEFAULT_DATE_LENGTH);
        if (cal.get(Calendar.ERA) == GregorianCalendar.BC) {
            buffer.append("-");
        }

        buffer.append(formatter.format(date));
        formatTimeZone(cal, buffer);
        return buffer.toString();
    } // -- format

    /**
     * Format the time zone information (only) from the provided Calendar.
     * @param cal a calendar containing a time and time zone
     * @param buffer the StringBuffer to which to format the time zone
     */
    private static void formatTimeZone(final Calendar cal, final StringBuffer buffer) {
        int value = cal.get(Calendar.ZONE_OFFSET);
        int dstOffset = cal.get(Calendar.DST_OFFSET);

        if (value == 0 && dstOffset == 0) {
            buffer.append('Z'); // UTC
            return;
        }

        if (_allowTimeZoneSuppression && value == _timezone.getRawOffset()) {
            return;
        }

        // -- adjust for Daylight Savings Time
        value = value + dstOffset;

        if (value > 0) {
            buffer.append('+');
        } else {
            value = -value;
            buffer.append('-');
        }

        // -- convert to minutes from milliseconds
        int minutes = value / 60000;

        // -- hours: hh
        value = minutes / 60;
        if (value < 10) {
            buffer.append('0');
        }
        buffer.append(value);
        buffer.append(':');

        // -- remaining minutes: mm
        value = minutes % 60;
        if (value < 10) {
            buffer.append('0');
        }
        buffer.append(value);
    }

    /**
     * Formats the given object. If the object is a java.util.Date, it will be
     * formatted by a call to {@link #format(Date)}, otherwise the toString()
     * method is called on the object.
     * @param object The object to be formatted
     *
     * @return the formatted object.
     */
    private static String format(final Object object) {
        if (object == null) {
            return null;
        }
        if (object instanceof java.util.Date) {
            return format((Date) object);
        }
        return object.toString();
    } //-- format

    /**
     * A class for controlling the parse options.  There is currently only one
     * parse option.
     */
    static class ParseOptions {
        /** If true and the 'T' field is not present, a xsd:date is parsed, else xsd:dateTime. */
        public boolean _allowNoTime = false;
    }

} //-- DateFieldHandler
