/*
 * 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 2001 (C) Intalio, Inc. All Rights Reserved.
 *
 * $Id: Time.java 8100 2009-02-22 16:16:33Z rjoachim $
 * Date         Author          Changes
 * 10/02/2002   Arnaud          Added support for 3 figure millisecond and clean up
 * 04/18/2002   Arnaud          String Constructor
 * 05/22/2001   Arnaud Blandin  Created
 */
package org.exolab.castor.types;

import java.text.SimpleDateFormat;
import java.text.ParseException;
import java.util.Date;

/**
 * Describes an XML schema Time.
 * <p>
 * The format is defined by W3C XML Schema Recommendation and ISO8601 i.e
 * <tt>hh:mm:ss.sss(Z|(+|-)hh:mm)</tt>
 * <p>
 * Deep support of fractional seconds is not implemented. This implementation
 * only supports fractional second resolution of milliseconds.
 *
 * @author <a href="mailto:blandin@intalio.com">Arnaud Blandin</a>
 * @author <a href="mailto:edward.kuns@aspect.com">Edward Kuns</a>
 * @version $Revision: 8100 $
 * @see DateTimeBase
 */
public class Time extends DateTimeBase {

    /** SerialVersionUID */
    private static final long serialVersionUID = -8268707778437931489L;

    /** The Time Format used by the toDate() method */
    private static final String TIME_FORMAT_MILLI = "HH:mm:ss.SSS";
    private static final String TIME_FORMAT_NO_MILLI = "HH:mm:ss";

    private static final String BAD_TIME = "Bad Time format: ";

    /**
     * No-arg constructor.
     */
    public Time() {
        // Nothing to do
    }

    /**
     * Constructs a XML Schema Time instance given all the values of
     * the different fields.
     * By default a Time is not UTC and is local.
     * @param values an array of shorts that represent the different fields of Time.
     */
    public Time(short[] values) {
        setValues(values);
    }

    /**
     * Constructs a XML Schema Time instance given a long representing the time in milliseconds.
     * By default a Time is not UTC and is local.
     * @param l The long value that represents the time instance.
     */
    public Time(long l) {
        this(l, false);
    }

    /**
     * Constructs a XML Schema Time instance given a long representing the time in milliseconds.
     * @param l The long value that represents the time instance.
     * @param utc Does the long value represent a UTC time?
     */
    public Time(long l, boolean utc) {
        if (l > 86400000L) {
            throw new IllegalArgumentException(
                    "Bad Time: the long value can't represent more than 24h.");
        }
        
        setHour((short) (l / 3600000));
        l = l % 3600000;
        setMinute((short) (l / 60000));
        l = l % 60000;
        setSecond((short) (l / 1000), (short) (l % 1000));
        
        if (utc) { setZone((short) 0, (short) 0); }
    }

   /**
    * Constructs a Time given a string representation.
    * @param time the string representation of the Time to instantiate
    * @throws ParseException a parse exception is thrown if the string to parse
    *                        does not follow the rigth format (see the description
    *                        of this class)
    */
    public Time(String time) throws ParseException {
       parseTimeInternal(time, this);
    }

    /**
     * Sets all the fields by reading the values in an array.
     * <p>
     * if a Time Zone is specified it has to be set by using
     * {@link DateTimeBase#setZone(short, short) setZone}.
     *
     * @param values
     *            an array of shorts with the values the array is supposed to be
     *            of length 4 and ordered like that:
     *            <ul>
     *            <li>hour</li>
     *            <li>minute</li>
     *            <li>second</li>
     *            <li>millisecond</li>
     *            </ul>
     */
     public void setValues(short[] values) {
        if (values.length != 4) {
             throw new IllegalArgumentException("Time#setValues: not the right number of values");
        }
        this.setHour(values[0]);
        this.setMinute(values[1]);
        this.setSecond(values[2],values[3]);
     }

    /**
     * returns an array of short with all the fields that describe
     * this time type.
     * <p>Note:the time zone is not included.
     * @return  an array of short with all the fields that describe
     * this time type.
     */
    public short[] getValues() {
        short[] result = new short[4];
        result[0] = this.getHour();
        result[1] = this.getMinute();
        result[2] = this.getSeconds();
        result[3] = this.getMilli();
        return result;
    } //getValues

    /**
     * Converts this Time instance into a long value based on UTC time zone.
     * @return A long value representing this Time instance.
     */
    public long toLong () {
        int sign = isZoneNegative() ? 1 : -1;
        int hour = getHour() + sign * getZoneHour();
        int minute = getMinute() + sign * getZoneMinute();
        int second = getSeconds();
        int milli = getMilli();
        
        if (minute < 0) {
            minute = minute + 60;
            hour = hour - 1;
        } else if (minute > 59) {
            minute = minute - 60;
            hour = hour + 1;
        }
        
        if (hour < 0) {
            hour = hour + 24;
        } else if (hour > 23) {
            hour = hour - 24;
        }
        
        return 3600000 * hour + 60000 * minute + 1000 * second + milli;
    }

    /**
     * converts this Time into a local java Date.
     * @return a local date representing this Time
     */
    public Date toDate() {
        Date date = null;
        SimpleDateFormat df = null;
        String temp = this.toString();
        if (temp.indexOf('.') > 0) {
            df = new SimpleDateFormat(TIME_FORMAT_MILLI);
        } else {
            df = new SimpleDateFormat(TIME_FORMAT_NO_MILLI);
        }

        setDateFormatTimeZone(df);

        try {
            date = df.parse(temp);
        } catch (ParseException e) {
            //this can't happen since toString() should return the proper string format
           e.printStackTrace();
           return null;
        }
        return date;
    } //toDate()

    /**
     * convert this Time to a string
     * The format is defined by W3C XML Schema Recommendation and ISO8601
     * i.e hh:mm:ss.sss(Z|(+|-)hh:mm)
     * @return a string representing this Time
     */
    public String toString() {
        StringBuffer result = new StringBuffer();
        appendTimeString(result);
        appendTimeZoneString(result);
        return result.toString();
    } //toString

   /**
    * parses a String and converts it into a java.lang.Object
    * @param str the string to parse
    * @return the java.lang.Object represented by the string
    * @throws ParseException a parse exception is thrown if the string to parse
    *                        does not follow the rigth format (see the description
    *                        of this class)
    */
    public static Object parse(String str) throws ParseException {
        return parseTime(str);
    }

    /**
     * parses a String and converts it into a Time.
     * @param str the string to parse
     * @return the Time represented by the string
     * @throws ParseException a parse exception is thrown if the string to parse
     *                        does not follow the rigth format (see the description
     *                        of this class)
     */
    public static Time parseTime(String str) throws ParseException {
        return parseTimeInternal(str, null);
    }

    private static Time parseTimeInternal(String str, Time result) throws ParseException {
        if (str == null) {
            throw new IllegalArgumentException("The string to be parsed must not be null.");
        }

        if (result == null) {
            result = new Time();
        }

        char[] chars = str.toCharArray();
        int idx = parseTime(str, result, chars, 0, BAD_TIME);
        parseTimeZone(str, result, chars, idx, BAD_TIME);

        return result;
    } //parse

    /////////////////////////// DISALLOWED METHODS ///////////////////////////

    public boolean hasIsNegative() {
        return false;
    }
    public boolean isNegative() {
        String err = "org.exolab.castor.types.Time does not have a 'negative' field.";
        throw new UnsupportedOperationException(err);
    }
    public void setNegative() {
        String err = "org.exolab.castor.types.Time cannot be negative.";
        throw new UnsupportedOperationException(err);
    }
    public boolean hasCentury() {
        return false;
    }
    public short getCentury() {
        String err = "org.exolab.castor.types.Time does not have a Century field.";
        throw new UnsupportedOperationException(err);
    }
    public void setCentury(short century) {
        String err = "org.exolab.castor.types.Time does not have a Century field.";
        throw new UnsupportedOperationException(err);
    }
    public boolean hasYear() {
        return false;
    }
    public short getYear() {
        String err = "org.exolab.castor.types.Time does not have a Year field.";
        throw new UnsupportedOperationException(err);
    }
    public void setYear(short year) {
        String err = "org.exolab.castor.types.Time does not have a Year field.";
        throw new UnsupportedOperationException(err);
    }
    public boolean hasMonth() {
        return false;
    }
    public short getMonth() {
        String err = "org.exolab.castor.types.Time does not have a Month field.";
        throw new UnsupportedOperationException(err);
    }
    public void setMonth(short month) {
        String err = "org.exolab.castor.types.Time does not have a Month field.";
        throw new UnsupportedOperationException(err);
    }
    public boolean hasDay() {
        return false;
    }
    public short getDay() {
        String err = "org.exolab.castor.types.Time does not have a Day field.";
        throw new UnsupportedOperationException(err);
    }
    public void setDay(short month) {
        String err = "org.exolab.castor.types.Time does not have a Day field.";
        throw new UnsupportedOperationException(err);
    }

} //Time
