/*
 * ====================================================================
 * Copyright (c) 2004-2012 TMate Software Ltd.  All rights reserved.
 *
 * This software is licensed as described in the file COPYING, which
 * you should have received as part of this distribution.  The terms
 * are also available at http://svnkit.com/license.html.
 * If newer versions of this license are posted there, you may use a
 * newer version instead, at your option.
 * ====================================================================
 */
package org.tmatesoft.svn.core.internal.util;

import java.util.Calendar;
import java.util.Date;
import java.util.Locale;
import java.util.TimeZone;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.text.FieldPosition;

import org.tmatesoft.svn.core.SVNErrorCode;
import org.tmatesoft.svn.core.SVNErrorMessage;
import org.tmatesoft.svn.core.SVNException;
import org.tmatesoft.svn.core.wc.ISVNOptions;
import org.tmatesoft.svn.core.internal.wc.SVNErrorManager;
import org.tmatesoft.svn.util.SVNDebugLog;
import org.tmatesoft.svn.util.SVNLogType;


/**
 * @author TMate Software Ltd.
 * @version 1.3
 */
public class SVNDate extends Date {

    private static final long serialVersionUID = 4845L;

    public static final SVNDate NULL = new SVNDate(0, 0);

    private static final Calendar CALENDAR = Calendar.getInstance(TimeZone.getTimeZone("GMT"), new Locale("en", "US"));

    static final DateFormat SVN_FORMAT = new SimpleDateFormat(
            "yyyy-MM-dd'T'HH:mm:ss.SSS");

    private static final DateFormat ISO8601_FORMAT = new SimpleDateFormat(
            "yyyy-MM-dd'T'HH:mm:ss.SSS'000Z'");

    private static final DateFormat RFC1123_FORMAT = new SimpleDateFormat(
            "EEE, d MMM yyyy HH:mm:ss z", Locale.US);

    private static final DateFormat CUSTOM_FORMAT = new SimpleDateFormat(
            "yyyy-MM-dd HH:mm:ss Z (EE, d MMM yyyy)", Locale.getDefault());

    private static final DateFormat HUMAN_FORMAT = new SimpleDateFormat(
            "yyyy-MM-dd' 'HH:mm:ss' 'ZZZZ' ('E', 'dd' 'MMM' 'yyyy')'");

    private static final DateFormat SHORT_FORMAT = new SimpleDateFormat(
            "yyyy-MM-dd' 'HH:mm:ss'Z'");

    private static final DateFormat CONSOLE_DIFF_DATE_FORMAT = new SimpleDateFormat(
            "EEE' 'MMM' 'dd' 'HH:mm:ss' 'yyyy");

    private static final DateFormat CONSOLE_LONG_DATE_FORMAT = new SimpleDateFormat(
            "MM' 'dd'  'yyyy");

    private static final DateFormat CONSOLE_SHORT_DATE_FORMAT = new SimpleDateFormat(
            "MM' 'dd'  'HH:mm");

    public static final char[] DATE_SEPARATORS = {'-', '-', 'T', ':', ':', '.', 'Z'};

    static {
        SVN_FORMAT.setTimeZone(TimeZone.getTimeZone("GMT"));
        ISO8601_FORMAT.setTimeZone(TimeZone.getTimeZone("GMT"));
        RFC1123_FORMAT.setTimeZone(TimeZone.getTimeZone("GMT"));
        HUMAN_FORMAT.setTimeZone(TimeZone.getTimeZone("GMT"));
        SHORT_FORMAT.setTimeZone(TimeZone.getTimeZone("GMT"));
        CUSTOM_FORMAT.setTimeZone(TimeZone.getTimeZone("GMT"));
    }

    private int myMicroSeconds;

    public SVNDate(long time, int micro) {
        super((1000 * time + micro) / 1000);
        myMicroSeconds = micro >= 0 ? micro % 1000 : 1000 + (micro % 1000);
    }

    public String format() {
        StringBuffer formatted = new StringBuffer();
        synchronized (SVN_FORMAT) {
            SVN_FORMAT.format(this, formatted, new FieldPosition(0));
        }
        int m1 = myMicroSeconds % 10;
        int m2 = (myMicroSeconds / 10) % 10;
        int m3 = (myMicroSeconds) / 100;
        formatted.append(m3);
        formatted.append(m2);
        formatted.append(m1);
        formatted.append('Z');
        return formatted.toString();
    }

    public static String formatDate(Date date) {
        return formatDate(date, false);
    }

    public static String formatDate(Date date, boolean formatZeroDate) {
        if (date == null) {
            return null;
        } else if (!formatZeroDate && date.getTime() == 0) {
            return null;
        }
        if (date instanceof SVNDate) {
            SVNDate extendedDate = (SVNDate) date;
            return extendedDate.format();
        }
        synchronized (ISO8601_FORMAT) {
            return ISO8601_FORMAT.format(date);
        }
    }

    public static String formatRFC1123Date(Date date) {
        if (date == null) {
            return null;
        }
        synchronized (RFC1123_FORMAT) {
            return RFC1123_FORMAT.format(date);
        }
    }

    public static String formatHumanDate(Date date, ISVNOptions options) {
        DateFormat df = HUMAN_FORMAT;
        if (options != null && options.getKeywordDateFormat() != null) {
            df = options.getKeywordDateFormat();
        }
        synchronized (df) {
            return df.format(date != null ? date : NULL);
        }
    }

    public static String formatShortDate(Date date) {
        synchronized (SHORT_FORMAT) {
            return SHORT_FORMAT.format(date != null ? date : NULL);
        }
    }

    public static String formatCustomDate(Date date) {
        if (date == null) {
            return null;
        }
        synchronized (CUSTOM_FORMAT) {
            return CUSTOM_FORMAT.format(date);
        }
    }

    public static String formatConsoleDiffDate(Date date){
        if (date == null) {
            return null;
        }
        synchronized (CONSOLE_DIFF_DATE_FORMAT) {
            return CONSOLE_DIFF_DATE_FORMAT.format(date);
        }
    }

    public static String formatConsoleLongDate(Date date){
        if (date == null) {
            return null;
        }
        synchronized (CONSOLE_LONG_DATE_FORMAT) {
            return CONSOLE_LONG_DATE_FORMAT.format(date);
        }
    }

    public static String formatConsoleShortDate(Date date){
        if (date == null) {
            return null;
        }
        synchronized (CONSOLE_SHORT_DATE_FORMAT) {
            return CONSOLE_SHORT_DATE_FORMAT.format(date);
        }
    }

    public static SVNDate parseDate(String str) {
        if (str == null) {
            return NULL;
        }
        try {
            return parseDatestamp(str);
        } catch (Throwable th) {
            SVNDebugLog.getDefaultLog().logFine(SVNLogType.DEFAULT, th);
        }
        return NULL;
    }

    public static Date parseDateString(String str) throws SVNException {
        try {
            return parseDatestamp(str);
        } catch (SVNException svne) {
            throw svne;
        } catch (Throwable th) {
            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.BAD_DATE);
            SVNErrorManager.error(err, th, SVNLogType.DEFAULT);
        }
        return NULL;
    }

    private static SVNDate parseDatestamp(String str) throws SVNException {
        if (str == null) {
            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.BAD_DATE);
            SVNErrorManager.error(err, SVNLogType.DEFAULT);
        }

        int index = 0;
        int charIndex = 0;
        int startIndex = 0;
        int[] result = new int[7];
        int microseconds = 0;
        int timeZoneInd = -1;
        while (index < DATE_SEPARATORS.length && charIndex < str.length()) {
            if (str.charAt(charIndex) == '-') {
                if (index > 1) {
                    timeZoneInd = charIndex;
                }
            } else if (str.charAt(charIndex) == '+') {
                timeZoneInd = charIndex;
            }
            if (str.charAt(charIndex) == DATE_SEPARATORS[index] ||
                    (index == 5 && str.charAt(charIndex) == DATE_SEPARATORS[index + 1])) {
                if (index == 5 && str.charAt(charIndex) == DATE_SEPARATORS[index + 1]) {
                    index++;
                }
                String segment = str.substring(startIndex, charIndex);
                if (segment.length() == 0) {
                    result[index] = 0;
                } else if (index + 1 < DATE_SEPARATORS.length) {
                    result[index] = Integer.parseInt(segment);
                } else {
                    result[index] = Integer.parseInt(segment.substring(0, Math.min(3, segment.length())));
                    if (segment.length() > 3) {
                        microseconds = Integer.parseInt(segment.substring(3));
                    }
                }
                startIndex = charIndex + 1;
                index++;
            }
            charIndex++;
        }
        if (index < DATE_SEPARATORS.length) {
            String segment = str.substring(startIndex);
            if (segment.length() == 0) {
                result[index] = 0;
            } else {
                result[index] = Integer.parseInt(segment);
            }
        }

        int year = result[0];
        int month = result[1];
        int date = result[2];

        int hour = result[3];
        int min = result[4];
        int sec = result[5];
        int ms = result[6];

        String timeZoneId = null;
        if (timeZoneInd != -1 && timeZoneInd < str.length() - 1 && str.indexOf('Z') == -1 && str.indexOf('z') == -1) {
            timeZoneId = "GMT" + str.substring(timeZoneInd);
        }
        synchronized (CALENDAR) {
            CALENDAR.clear();
            TimeZone oldTimeZone = null;
            if (timeZoneId != null) {
                oldTimeZone = CALENDAR.getTimeZone();
                CALENDAR.setTimeZone(TimeZone.getTimeZone(timeZoneId));
            } else if (str.indexOf('Z') == -1 && str.indexOf('z') == -1) {
                oldTimeZone = CALENDAR.getTimeZone();
                CALENDAR.setTimeZone(TimeZone.getDefault());
            }

            CALENDAR.set(year, month - 1, date, hour, min, sec);
            CALENDAR.set(Calendar.MILLISECOND, ms);
            SVNDate resultDate = new SVNDate(CALENDAR.getTimeInMillis(), microseconds);
            if (oldTimeZone != null) {
                CALENDAR.setTimeZone(oldTimeZone);
            }
            return resultDate;
        }
    }

    public static long parseDateAsMilliseconds(String str) {
        if (str == null) {
            return -1;
        }
        int index = 0;
        int charIndex = 0;
        int startIndex = 0;
        int[] result = new int[7];
        while (index < DATE_SEPARATORS.length && charIndex < str.length()) {
            if (str.charAt(charIndex) == DATE_SEPARATORS[index]) {
                String segment = str.substring(startIndex, charIndex);
                if (segment.length() == 0) {
                    result[index] = 0;
                } else if (index + 1 < DATE_SEPARATORS.length) {
                    try {
                        result[index] = Integer.parseInt(segment);
                    } catch (NumberFormatException nfe) {
                        return -1;
                    }
                } else {
                    try {
                        result[index] = Integer.parseInt(segment.substring(0, Math.min(3, segment.length())));
                    } catch (NumberFormatException nfe) {
                        return -1;
                    }
                }
                startIndex = charIndex + 1;
                index++;
            }
            charIndex++;
        }
        int year = result[0];
        int month = result[1];
        int date = result[2];

        int hour = result[3];
        int min = result[4];
        int sec = result[5];
        int ms = result[6];

        synchronized (CALENDAR) {
            CALENDAR.clear();
            CALENDAR.set(year, month - 1, date, hour, min, sec);
            CALENDAR.set(Calendar.MILLISECOND, ms);
            return CALENDAR.getTimeInMillis();
        }
    }
    
    public static SVNDate fromDate(Date d) {
        if (d == null) {
            return SVNDate.NULL;
        }
        if (d instanceof SVNDate) {
            return (SVNDate) d;
        }
        return new SVNDate(d.getTime(), 0);
    }

    public int hashCode() {
        return 31 * super.hashCode() + myMicroSeconds;
    }

    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        } else if (obj instanceof SVNDate) {
            SVNDate date = (SVNDate) obj;
            return getTime() == date.getTime() && myMicroSeconds == date.myMicroSeconds;
        }
        return super.equals(obj);
    }

    public boolean before(Date when) {
        if (super.equals(when) && when instanceof SVNDate) {
            return myMicroSeconds < ((SVNDate) when).myMicroSeconds;
        }
        return super.before(when);
    }

    public boolean after(Date when) {
        if (super.equals(when) && when instanceof SVNDate) {
            return myMicroSeconds > ((SVNDate) when).myMicroSeconds;
        }
        return super.after(when);
    }

    public int compareTo(Date anotherDate) {
        int result = super.compareTo(anotherDate);
        if (result == 0 && anotherDate instanceof SVNDate) {
            SVNDate date = (SVNDate) anotherDate;
            return (myMicroSeconds < date.myMicroSeconds ? -1 : (myMicroSeconds == date.myMicroSeconds ? 0 : 1));
        }
        return result;
    }

    public long getTimeInMicros() {
        return 1000 * getTime() + myMicroSeconds;
    }
}
