/*****************************************************************************
 * 
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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.xmpbox;

import java.io.IOException;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.SimpleTimeZone;

/**
 * This class is used to convert dates to strings and back using the PDF date standards. Date are described in
 * PDFReference1.4 section 3.8.2
 * 
 * @author <a href="mailto:ben@benlitchfield.com">Ben Litchfield</a>
 * @author <a href="mailto:chris@oezbek.net">Christopher Oezbek</a>
 * 
 * @version $Revision: 1.3 $
 */
public class DateConverter
{

    // The Date format is supposed to be the PDF_DATE_FORMAT, but not all PDF
    // documents
    // will use that date, so I have added a couple other potential formats
    // to try if the original one does not work.
    private static final SimpleDateFormat[] POTENTIAL_FORMATS = new SimpleDateFormat[] {
            new SimpleDateFormat("EEEE, dd MMM yyyy hh:mm:ss a"),
            new SimpleDateFormat("EEEE, MMM dd, yyyy hh:mm:ss a"), new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"),
            new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssz") };

    /**
     * According to check-style, Utility classes should not have a public or default constructor.
     */
    protected DateConverter()
    {
    };

    /**
     * This will convert a string to a calendar.
     * 
     * @param date
     *            The string representation of the calendar.
     * 
     * @return The calendar that this string represents.
     * 
     * @throws IOException
     *             If the date string is not in the correct format.
     */
    public static Calendar toCalendar(String date) throws IOException
    {
        Calendar retval = null;
        if ((date != null) && (date.trim().length() > 0))
        {
            // these are the default values
            int year = 0;
            int month = 1;
            int day = 1;
            int hour = 0;
            int minute = 0;
            int second = 0;
            // first string off the prefix if it exists
            try
            {
                SimpleTimeZone zone = null;
                if (date.startsWith("D:"))
                {
                    date = date.substring(2, date.length());
                }

                date = date.replaceAll("[-:T]", "");

                if (date.length() < 4)
                {
                    throw new IOException("Error: Invalid date format '" + date + "'");
                }
                year = Integer.parseInt(date.substring(0, 4));
                if (date.length() >= 6)
                {
                    month = Integer.parseInt(date.substring(4, 6));
                }
                if (date.length() >= 8)
                {
                    day = Integer.parseInt(date.substring(6, 8));
                }
                if (date.length() >= 10)
                {
                    hour = Integer.parseInt(date.substring(8, 10));
                }
                if (date.length() >= 12)
                {
                    minute = Integer.parseInt(date.substring(10, 12));
                }

                int timeZonePos = 12;
                if (date.length() - 12 > 5 || (date.length() - 12 == 3 && date.endsWith("Z")))
                {
                    if (date.length() >= 14)
                    {
                        second = Integer.parseInt(date.substring(12, 14));
                    }
                    timeZonePos = 14;
                }
                else
                {
                    second = 0;
                }

                if (date.length() >= (timeZonePos + 1))
                {
                    char sign = date.charAt(timeZonePos);
                    if (sign == 'Z')
                    {
                        zone = new SimpleTimeZone(0, "Unknown");
                    }
                    else
                    {
                        int hours = 0;
                        int minutes = 0;
                        if (date.length() >= (timeZonePos + 3))
                        {
                            if (sign == '+')
                            {
                                // parseInt cannot handle the + sign
                                hours = Integer.parseInt(date.substring((timeZonePos + 1), (timeZonePos + 3)));
                            }
                            else
                            {
                                hours = -Integer.parseInt(date.substring(timeZonePos, (timeZonePos + 2)));
                            }
                        }
                        if (sign == '+')
                        {
                            if (date.length() >= (timeZonePos + 5))
                            {
                                minutes = Integer.parseInt(date.substring((timeZonePos + 3), (timeZonePos + 5)));
                            }
                        }
                        else
                        {
                            if (date.length() >= (timeZonePos + 4))
                            {
                                minutes = Integer.parseInt(date.substring((timeZonePos + 2), (timeZonePos + 4)));
                            }
                        }
                        zone = new SimpleTimeZone(hours * 60 * 60 * 1000 + minutes * 60 * 1000, "Unknown");
                    }
                }

                if (zone == null)
                {
                    retval = new GregorianCalendar();
                }
                else
                {
                    retval = new GregorianCalendar(zone);
                }
                retval.clear();
                retval.set(year, month - 1, day, hour, minute, second);
            }
            catch (NumberFormatException e)
            {

                // remove the arbitrary : in the timezone. SimpleDateFormat
                // can't handle it
                if (date.substring(date.length() - 3, date.length() - 2).equals(":")
                        && (date.substring(date.length() - 6, date.length() - 5).equals("+") || date.substring(
                                date.length() - 6, date.length() - 5).equals("-")))
                {
                    // thats a timezone string, remove the :
                    date = date.substring(0, date.length() - 3) + date.substring(date.length() - 2);
                }
                for (int i = 0; (retval == null) && (i < POTENTIAL_FORMATS.length); i++)
                {
                    try
                    {
                        Date utilDate = POTENTIAL_FORMATS[i].parse(date);
                        retval = new GregorianCalendar();
                        retval.setTime(utilDate);
                    }
                    catch (ParseException pe)
                    {
                        // ignore and move to next potential format
                    }
                }
                if (retval == null)
                {
                    // we didn't find a valid date format so throw an exception
                    IOException ioe = new IOException("Error converting date:" + date);
                    ioe.initCause(e);
                    throw ioe;
                }
            }
        }
        return retval;
    }

    /**
     * Append Zero to String Buffer before number &lt; 10 ('1' become '01')
     * 
     * @param out
     *            The String buffer
     * @param number
     *            The concerned number
     */
    private static void zeroAppend(StringBuffer out, int number)
    {
        if (number < 10)
        {
            out.append("0");
        }
        out.append(number);
    }

    /**
     * Convert the date to iso 8601 string format.
     * 
     * @param cal
     *            The date to convert.
     * @return The date represented as an ISO 8601 string.
     */
    public static String toISO8601(Calendar cal)
    {
        StringBuffer retval = new StringBuffer();

        retval.append(cal.get(Calendar.YEAR));
        retval.append("-");
        zeroAppend(retval, cal.get(Calendar.MONTH) + 1);
        retval.append("-");
        zeroAppend(retval, cal.get(Calendar.DAY_OF_MONTH));
        retval.append("T");
        zeroAppend(retval, cal.get(Calendar.HOUR_OF_DAY));
        retval.append(":");
        zeroAppend(retval, cal.get(Calendar.MINUTE));
        retval.append(":");
        zeroAppend(retval, cal.get(Calendar.SECOND));

        int timeZone = cal.get(Calendar.ZONE_OFFSET) + cal.get(Calendar.DST_OFFSET);
        if (timeZone < 0)
        {
            retval.append("-");
        }
        else
        {
            retval.append("+");
        }
        timeZone = Math.abs(timeZone);
        // milliseconds/1000 = seconds = seconds / 60 = minutes = minutes/60 =
        // hours
        int hours = timeZone / 1000 / 60 / 60;
        int minutes = (timeZone - (hours * 1000 * 60 * 60)) / 1000 / 1000;
        if (hours < 10)
        {
            retval.append("0");
        }
        retval.append(Integer.toString(hours));
        retval.append(":");
        if (minutes < 10)
        {
            retval.append("0");
        }
        retval.append(Integer.toString(minutes));
        return retval.toString();
    }
}
