/*
 * Copyright 2002-2004 The Apache Software Foundation.
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.axis.types;

import org.apache.axis.utils.Messages;

import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.TimeZone;

/**
 * Class that represents the xsd:time XML Schema type
 */
public class Time implements java.io.Serializable {
    private Calendar _value;


    /**
     * a shared java.text.SimpleDateFormat instance used for parsing the basic
     * component of the timestamp
     */
    private static SimpleDateFormat zulu =
       new SimpleDateFormat("HH:mm:ss.SSS'Z'");

    // We should always format dates in the GMT timezone
    static {
        zulu.setTimeZone(TimeZone.getTimeZone("GMT"));
    }


    /**
     * Initialize with a Calender, year month and date are ignored
     */
    public Time(Calendar value) {
        this._value = value;
        _value.set(0,0,0);      // ignore year, month, date
    }

    /**
     * Converts a string formatted as HH:mm:ss[.SSS][+/-offset]
     */
    public Time(String value) throws NumberFormatException {
        _value = makeValue(value);
    }

    /**
     * return the time as a calendar: ignore the year, month and date fields
     * @return calendar value; may be null
     */
    public Calendar getAsCalendar() {
        return _value;
    }

    /**
     * set the time; ignore year, month, date
     * @param date
     */
    public void setTime(Calendar date) {
        this._value = date;
        _value.set(0,0,0);      // ignore year, month, date
    }

    /**
     * set the time from a date instance
     * @param date
     */
    public void setTime(Date date) {
        _value.setTime(date);
        _value.set(0,0,0);      // ignore year, month, date
    }

    /**
     * Utility function that parses xsd:time strings and returns a Date object
     */
    private Calendar makeValue(String source) throws NumberFormatException {
        Calendar calendar = Calendar.getInstance();
        Date date;

        validateSource(source);

        // convert what we have validated so far
        date = ParseHoursMinutesSeconds(source);

        int pos = 8;    // The "." in hh:mm:ss.sss

        // parse optional milliseconds
        if ( source != null ) {
            if (pos < source.length() && source.charAt(pos)=='.') {
                int milliseconds = 0;
                int start = ++pos;
                while (pos<source.length() &&
                       Character.isDigit(source.charAt(pos))) {
                    pos++;
                }


                String decimal=source.substring(start,pos);
                if (decimal.length()==3) {
                    milliseconds=Integer.parseInt(decimal);
                } else if (decimal.length() < 3) {
                    milliseconds=Integer.parseInt((decimal+"000")
                                                  .substring(0,3));
                } else {
                    milliseconds=Integer.parseInt(decimal.substring(0,3));
                    if (decimal.charAt(3)>='5') {
                        ++milliseconds;
                    }
                }

                // add milliseconds to the current date
                date.setTime(date.getTime()+milliseconds);
            }

            // parse optional timezone
            if (pos+5 < source.length() &&
                (source.charAt(pos)=='+' || (source.charAt(pos)=='-'))) {
                    if (!Character.isDigit(source.charAt(pos+1)) ||
                        !Character.isDigit(source.charAt(pos+2)) ||
                        source.charAt(pos+3) != ':'              ||
                        !Character.isDigit(source.charAt(pos+4)) ||
                        !Character.isDigit(source.charAt(pos+5)))
                    {
                        throw new NumberFormatException(
                                Messages.getMessage("badTimezone00"));
                    }

                    int hours = (source.charAt(pos+1)-'0')*10
                        +source.charAt(pos+2)-'0';
                    int mins  = (source.charAt(pos+4)-'0')*10
                        +source.charAt(pos+5)-'0';
                    int milliseconds = (hours*60+mins)*60*1000;

                    // subtract milliseconds from current date to obtain GMT
                    if (source.charAt(pos)=='+') {
                        milliseconds=-milliseconds;
                    }
                    date.setTime(date.getTime()+milliseconds);
                    pos+=6;
            }

            if (pos < source.length() && source.charAt(pos)=='Z') {
                pos++;
                calendar.setTimeZone(TimeZone.getTimeZone("GMT"));
            }

            if (pos < source.length()) {
                throw new NumberFormatException(
                        Messages.getMessage("badChars00"));
            }
        }

        calendar.setTime(date);
        calendar.set(0,0,0);    // ignore year, month, date

        return calendar;
    }

    private int getTimezoneNumberValue(char c) {
        int n=c-'0';
        if(n<0 || n>9) {
            //oops, out of range
            throw new NumberFormatException(
                    Messages.getMessage("badTimezone00"));
        }
        return n;
    }

    /**
     * parse the hours, minutes and seconds of a string, by handing it off to
     * the java runtime.
     * The relevant code will return null if a null string is passed in, so this
     * code may return a null date in response
     * @param source
     * @return
     * @throws NumberFormatException in the event of trouble
     */
    private static Date ParseHoursMinutesSeconds(String source) {
        Date date;
        try {
            synchronized (zulu) {
                String fulltime = source == null ? null :
                                                    (source.substring(0,8)+".000Z");
                date = zulu.parse(fulltime);
            }
        } catch (Exception e) {
            throw new NumberFormatException(e.toString());
        }
        return date;
    }

    /**
     * validate the source
     * @param source
     */
    private void validateSource(String source) {
        // validate fixed portion of format
        if ( source != null ) {
            if (source.charAt(2) != ':' || source.charAt(5) != ':') {
                throw new NumberFormatException(
                        Messages.getMessage("badTime00"));
            }
            if (source.length() < 8) {
                throw new NumberFormatException(
                        Messages.getMessage("badTime00"));
            }
        }
    }

    /**
     * stringify method returns the time as it would be in GMT, only accurate to the
     * second...millis probably get lost.
     * @return
     */
    public String toString() {
        if(_value==null) {
            return "unassigned Time";
        }
        synchronized (zulu) {
            return zulu.format(_value.getTime());
        }

    }

    public boolean equals(Object obj) {
        if (obj == null) return false;
        if (!(obj instanceof Time)) return false;
        Time other = (Time) obj;
        if (this == obj) return true;

        boolean _equals;
        _equals = true &&
            ((_value ==null && other._value ==null) ||
             (_value !=null &&
              _value.getTime().equals(other._value.getTime())));

        return _equals;

    }

    /**
     * Returns the hashcode of the underlying calendar.
     *
     * @return an <code>int</code> value
     */
    public int hashCode() {
        return _value == null ? 0 : _value.hashCode();
    }
}
