// serverDate.java 
// -------------------------------------------
// (C) by Michael Peter Christen; mc@anomic.de
// first published on http://www.anomic.de
// Frankfurt, Germany, 2005
// last major change: 14.03.2005
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 2 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
//
// Using this software in any meaning (reading, learning, copying, compiling,
// running) means that you agree that the Author(s) is (are) not responsible
// for cost, loss of data or any harm that may be caused directly or indirectly
// by usage of this softare or this documentation. The usage of this software
// is on your own risk. The installation and usage (starting/running) of this
// software may allow other people or application to access your computer and
// any attached devices and is highly dependent on the configuration of the
// software which must be done by the user of the software; the author(s) is
// (are) also not responsible for proper configuration and usage of the
// software, even if provoked by documentation provided together with
// the software.
//
// Any changes to this file according to the GPL as documented in the file
// gpl.txt aside this file in the shipment you received can be done to the
// lines that follows this copyright notice here, but changes must not be
// done inside the copyright notive above. A re-distribution must contain
// the intact and unchanged copyright notice.
// Contributions and changes to the program code must be marked as such.


// this class is needed to replace the slow java built-in date method by a faster version

package de.anomic.server;

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

public final class serverDate {
    
    
    // statics
    private final static long secondMillis = 1000;
    private final static long minuteMillis = 60 * secondMillis;
    private final static long hourMillis = 60 * minuteMillis;
    private final static long dayMillis = 24 * hourMillis;
    private final static long normalyearMillis = 365 * dayMillis;
    private final static long leapyearMillis = 366 * dayMillis;
    private final static int january = 31, normalfebruary = 28, leapfebruary = 29, march = 31,
                             april = 30, may = 31, june = 30, july = 31, august = 31,
                             september = 30, october = 31, november = 30, december = 31;
    private final static int[] dimnormal = {january, normalfebruary, march, april, may, june, july, august, september, october, november, december};
    private final static int[] dimleap = {january, leapfebruary, march, april, may, june, july, august, september, october, november, december};
    private final static String[] wkday = {"Mon","Tue","Wed","Thu","Fri","Sat","Sun"};
    private final static String[] month = {"Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"};

    // find out time zone and DST offset
    private static Calendar thisCalendar = GregorianCalendar.getInstance();
    //private static long zoneOffsetHours = thisCalendar.get(Calendar.ZONE_OFFSET);
    //private static long DSTOffsetHours = thisCalendar.get(Calendar.DST_OFFSET);
    //private static long offsetHours = zoneOffsetHours + DSTOffsetHours; // this must be subtracted from current Date().getTime() to produce a GMT Time

    // pre-calculation of time tables
    private final static long[] dimnormalacc, dimleapacc;
    private static long[] utimeyearsacc;
    static {
        long millis = 0;
        utimeyearsacc = new long[67];
        for (int i = 0; i < 67; i++) {
            utimeyearsacc[i] = millis;
            millis += ((i & 3) == 0) ? leapyearMillis : normalyearMillis;
        }
        millis = 0;
        dimnormalacc = new long[12];
        for (int i = 0; i < 12; i++) {
            dimnormalacc[i] = millis;
            millis += (dayMillis * dimnormal[i]);
        }
        millis = 0;
        dimleapacc = new long[12];
        for (int i = 0; i < 12; i++) {
            dimleapacc[i] = millis;
            millis += (dayMillis * dimleap[i]);
        }
    }
    
    // class variables
    private int milliseconds, seconds, minutes, hours, days, months, years; // years since 1970
    private int dow; // day-of-week
    private long utime;

    public static String UTCDiffString() {
        // we express the UTC Difference in 5 digits:
        // SHHMM
        // S  ::= '+'|'-'
        // HH ::= '00'|'01'|'02'|'03'|'04'|'05'|'06'|'07'|'08'|'09'|'10'|'11'|'12'
        // MM ::= '00'|'15'|'30'|'45'
        // since there are some places on earth where there is a time shift of half an hour
        // we need too show also the minutes of the time shift
        // Examples: http://www.timeanddate.com/library/abbreviations/timezones/
        long offsetHours = UTCDiff();
        int om = Math.abs((int) (offsetHours / minuteMillis)) % 60;
        int oh = Math.abs((int) (offsetHours / hourMillis));
        String diff = Integer.toString(om);
        if (diff.length() < 2) diff = "0" + diff;
        diff = Integer.toString(oh) + diff;
        if (diff.length() < 4) diff = "0" + diff;
        if (offsetHours >= 0) {
            return "+" + diff;
        } else {
            return "-" + diff;
        }
    }

    public static long UTCDiff() {
        long zoneOffsetHours = thisCalendar.get(Calendar.ZONE_OFFSET);
        long DSTOffsetHours = thisCalendar.get(Calendar.DST_OFFSET);
        return zoneOffsetHours + DSTOffsetHours;
    }
    
    public static long UTCDiff(String diffString) {
        if (diffString.length() != 5) throw new RuntimeException("UTC String malformed (wrong size):" + diffString);
        boolean ahead = true;
        if (diffString.charAt(0) == '+') ahead = true;
        else if (diffString.charAt(0) == '-') ahead = false;
        else throw new RuntimeException("UTC String malformed (wrong sign):" + diffString);
        long oh = Long.parseLong(diffString.substring(1, 3));
        long om = Long.parseLong(diffString.substring(3));
        return ((ahead) ? (long) 1 : (long) -1) * (oh * hourMillis + om * minuteMillis);
    }
    
    /*
    public static Date UTC0Date() {
        return new Date(UTC0Time());
    }
    */

    public static long correctedUTCTime() {
        return System.currentTimeMillis() - UTCDiff();
    }
    
    public serverDate() {
        this(System.currentTimeMillis());
    }
    
    public serverDate(long utime) {
        // set the time as the difference, measured in milliseconds,
        // between the current time and midnight, January 1, 1970 UTC/GMT
        this.utime = utime;
        dow = (int) (((utime / dayMillis) + 3) % 7);
        years = (int) (utime / normalyearMillis); // a guess
        if (utime < utimeyearsacc[years]) years--; // the correction
        long remain = utime - utimeyearsacc[years];
        months = (int) (remain / (29 * dayMillis)); // a guess
        if ((years & 3) == 0) {
            if (remain < dimleapacc[months]) months--; // correction
            remain = remain - dimleapacc[months];           
        } else {
            if (remain < dimnormalacc[months]) months--; // correction
            remain = remain - dimnormalacc[months];
        }
        days = (int) (remain / dayMillis); remain = remain % dayMillis;
        hours = (int) (remain / hourMillis); remain = remain % hourMillis;
        minutes = (int) (remain / minuteMillis); remain = remain % minuteMillis;
        seconds = (int) (remain / secondMillis); remain = remain % secondMillis;
        milliseconds = (int) remain;
    }

    private void calcUTime() {
        this.utime = utimeyearsacc[years] + dimleapacc[months - 1] + dayMillis * (days - 1) +
                     hourMillis * hours + minuteMillis * minutes + secondMillis * seconds + milliseconds;
        this.dow = (int) (((utime / dayMillis) + 3) % 7);
    }
        
    public serverDate(String datestring) throws java.text.ParseException {
        // parse a date string; othervise throw a java.text.ParseException
        if ((datestring.length() == 14) || (datestring.length() == 17)) {
            // parse a ShortString
            try {years = Integer.parseInt(datestring.substring(0, 4)) - 1970;} catch (NumberFormatException e) {
                throw new java.text.ParseException("serverDate '" + datestring + "' wrong year", 0);
            }
            if (years < 0) throw new java.text.ParseException("serverDate '" + datestring + "' wrong year", 0);
            try {months = Integer.parseInt(datestring.substring(4, 6)) - 1;} catch (NumberFormatException e) {
                throw new java.text.ParseException("serverDate '" + datestring + "' wrong month", 4);
            }
            if ((months < 0) || (months > 11)) throw new java.text.ParseException("serverDate '" + datestring + "' wrong month", 4);
            try {days = Integer.parseInt(datestring.substring(6, 8)) - 1;} catch (NumberFormatException e) {
                throw new java.text.ParseException("serverDate '" + datestring + "' wrong day", 6);
            }
            if ((days < 0) || (days > 30)) throw new java.text.ParseException("serverDate '" + datestring + "' wrong day", 6);
            try {hours = Integer.parseInt(datestring.substring(8, 10));} catch (NumberFormatException e) {
                throw new java.text.ParseException("serverDate '" + datestring + "' wrong hour", 8);
            }
            if ((hours < 0) || (hours > 23)) throw new java.text.ParseException("serverDate '" + datestring + "' wrong hour", 8);
            try {minutes = Integer.parseInt(datestring.substring(10, 12));} catch (NumberFormatException e) {
                throw new java.text.ParseException("serverDate '" + datestring + "' wrong minute", 10);
            }
            if ((minutes < 0) || (minutes > 59)) throw new java.text.ParseException("serverDate '" + datestring + "' wrong minute", 10);
            try {seconds = Integer.parseInt(datestring.substring(12, 14));} catch (NumberFormatException e) {
                throw new java.text.ParseException("serverDate '" + datestring + "' wrong second", 12);
            }
            if ((seconds < 0) || (seconds > 59)) throw new java.text.ParseException("serverDate '" + datestring + "' wrong second", 12);
            if (datestring.length() == 17) {
                try {milliseconds = Integer.parseInt(datestring.substring(14, 17));} catch (NumberFormatException e) {
                    throw new java.text.ParseException("serverDate '" + datestring + "' wrong millisecond", 14);
                }
            } else {
                milliseconds = 0;
            }
            if ((milliseconds < 0) || (milliseconds > 999)) throw new java.text.ParseException("serverDate '" + datestring + "' wrong millisecond", 14);
            calcUTime();
            return;
        }
        throw new java.text.ParseException("serverDate '" + datestring + "' format unknown", 0);
    }
    
    public String toString() {
        return "utime=" + utime + ", year=" + (years + 1970) +
               ", month=" + (months + 1) + ", day=" + (days + 1) +
               ", hour=" + hours + ", minute=" + minutes +
               ", second=" + seconds + ", millis=" + milliseconds +
               ", day-of-week=" + wkday[dow];
    }
    
    public String toShortString(boolean millis) {
        // returns a "yyyyMMddHHmmssSSS"
        byte[] result = new byte[(millis) ? 17 : 14];
        int x = 1970 + years;
        result[ 0] = (byte) (48 + (x / 1000)); x = x % 1000;
        result[ 1] = (byte) (48 + (x / 100)); x = x % 100;
        result[ 2] = (byte) (48 + (x / 10)); x = x % 10;
        result[ 3] = (byte) (48 + x);
        x = months + 1;
        result[ 4] = (byte) (48 + (x / 10));
        result[ 5] = (byte) (48 + (x % 10));
        x = days + 1;
        result[ 6] = (byte) (48 + (x / 10));
        result[ 7] = (byte) (48 + (x % 10));
        result[ 8] = (byte) (48 + (hours / 10));
        result[ 9] = (byte) (48 + (hours % 10));
        result[10] = (byte) (48 + (minutes / 10));
        result[11] = (byte) (48 + (minutes % 10));
        result[12] = (byte) (48 + (seconds / 10));
        result[13] = (byte) (48 + (seconds % 10));
        if (millis) {
            x = milliseconds;
            result[14] = (byte) (48 + (x / 100)); x = x % 100;
            result[15] = (byte) (48 + (x / 10)); x = x % 10;
            result[16] = (byte) (48 + x);
        }
        return new String(result);
    }
    
    // the following is only here to compare the kelondroDate with java-Date:
    private static TimeZone GMTTimeZone = TimeZone.getTimeZone("GMT");
    private static Calendar gregorian = new GregorianCalendar(GMTTimeZone);
    private static SimpleDateFormat testSFormatter = new SimpleDateFormat("yyyyMMddHHmmss");
    private static SimpleDateFormat testLFormatter = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss 'GMT'", Locale.US);

    public static String testSDateShortString() {
	return testSFormatter.format(gregorian.getTime());
    }
    
    public static String intervalToString(long millis) {
        try {
            long mins = millis / 60000;
            
            StringBuffer uptime = new StringBuffer();
            
            int uptimeDays  = (int) (Math.floor(mins/1440));
            int uptimeHours = (int) (Math.floor(mins/60)%24);
            int uptimeMins  = (int) mins%60;
            
            uptime.append(uptimeDays)
                  .append(((uptimeDays == 1)?" day ":" days "))
                  .append((uptimeHours < 10)?"0":"")
                  .append(uptimeHours)
                  .append(":")
                  .append((uptimeMins < 10)?"0":"")
                  .append(uptimeMins);            
            
            return uptime.toString();       
        } catch (Exception e) {
            return "unknown";
        }
    }
        
    public static void main(String[] args) {
        //System.out.println("kelondroDate is (" + new kelondroDate().toString() + ")");
        System.out.println("offset is " + (UTCDiff()/1000/60/60) + " hours, javaDate is " + new Date() + ", correctedDate is " + new Date(correctedUTCTime()));
        System.out.println("serverDate : " + new serverDate().toShortString(false));
        System.out.println("  javaDate : " + testSDateShortString());
        System.out.println("serverDate : " + new serverDate().toString());
        System.out.println("  JavaDate : " + testLFormatter.format(new Date()));
        System.out.println("serverDate0: " + new serverDate(0).toShortString(false));
        System.out.println("  JavaDate0: " + testSFormatter.format(new Date(0)));
        System.out.println("serverDate0: " + new serverDate(0).toString());
        System.out.println("  JavaDate0: " + testLFormatter.format(new Date(0)));
        // parse test
        try {
            System.out.println("serverDate re-parse short: " + new serverDate(new serverDate().toShortString(false)).toShortString(true));
            System.out.println("serverDate re-parse long : " + new serverDate(new serverDate().toShortString(true)).toShortString(true));
        } catch (java.text.ParseException e) {
            System.out.println("Parse Exception: " + e.getMessage() + ", pos " + e.getErrorOffset());
        }
        String testresult;
        int cycles = 10000;
        long start;
        
        start = System.currentTimeMillis();
        for (int i = 0; i < cycles; i++) testresult = new serverDate().toShortString(false);
        System.out.println("time for " + cycles + " calls to serverDate:" + (System.currentTimeMillis() - start) + " milliseconds");
        
        start = System.currentTimeMillis();
        for (int i = 0; i < cycles; i++) testresult = testSDateShortString();
        System.out.println("time for " + cycles + " calls to   javaDate:" + (System.currentTimeMillis() - start) + " milliseconds");
    }    
}