// Copyright (c) Corporation for National Research Initiatives

// An implementation of the Python standard time module.  Currently
// unimplemented:
//
// accept2dyear
//
// There may also be some incompatibilities in strftime(), because the Java
// tools for creating those formats don't always map to C's strftime()
// function.
//
// see org/python/modules/time.java for previous history.
package org.python.modules.time;

import java.text.DateFormatSymbols;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.TimeZone;
import java.text.ParseException;
import java.text.SimpleDateFormat;

import org.python.core.ClassDictInit;
import org.python.core.Py;
import org.python.core.PyBuiltinFunctionSet;
import org.python.core.PyException;
import org.python.core.PyInteger;
import org.python.core.PyObject;
import org.python.core.PyString;
import org.python.core.PyTuple;
import org.python.core.__builtin__;

class TimeFunctions extends PyBuiltinFunctionSet
{
    public TimeFunctions(String name, int index, int argcount) {
        super(name, index, argcount);
    }

    public PyObject __call__() {
        switch (index) {
        case 0:
            return Py.newFloat(Time.time());
        case 1:
            return Py.newFloat(Time.clock());
        default:
            throw info.unexpectedCall(0, false);
        }
    }
}

public class Time implements ClassDictInit
{
    public static PyString __doc__ = new PyString(
        "This module provides various functions to manipulate time values.\n"+
        "\n"+
        "There are two standard representations of time.  One is the "+
                "number\n"+
        "of seconds since the Epoch, in UTC (a.k.a. GMT).  It may be an "+
                "integer\n"+
        "or a floating point number (to represent fractions of seconds).\n"+
        "The Epoch is system-defined; on Unix, it is generally "+
                "January 1st, 1970.\n"+
        "The actual value can be retrieved by calling gmtime(0).\n"+
        "\n"+
        "The other representation is a tuple of 9 integers giving "+
                "local time.\n"+
        "The tuple items are:\n"+
        "  year (four digits, e.g. 1998)\n"+
        "  month (1-12)\n"+
        "  day (1-31)\n"+
        "  hours (0-23)\n"+
        "  minutes (0-59)\n"+
        "  seconds (0-59)\n"+
        "  weekday (0-6, Monday is 0)\n"+
        "  Julian day (day in the year, 1-366)\n"+
        "  DST (Daylight Savings Time) flag (-1, 0 or 1)\n"+
        "If the DST flag is 0, the time is given in the regular time zone;\n"+
        "if it is 1, the time is given in the DST time zone;\n"+
        "if it is -1, mktime() should guess based on the date and time.\n"+
        "\n"+
        "Variables:\n"+
        "\n"+
        "timezone -- difference in seconds between UTC and local "+
                "standard time\n"+
        "altzone -- difference in  seconds between UTC and local DST time\n"+
        "daylight -- whether local time should reflect DST\n"+
        "tzname -- tuple of (standard time zone name, DST time zone name)\n"+
        "\n"+
        "Functions:\n"+
        "\n"+
        "time() -- return current time in seconds since the Epoch "+
                "as a float\n"+
        "clock() -- return CPU time since process start as a float\n"+
        "sleep() -- delay for a number of seconds given as a float\n"+
        "gmtime() -- convert seconds since Epoch to UTC tuple\n"+
        "localtime() -- convert seconds since Epoch to local time tuple\n"+
        "asctime() -- convert time tuple to string\n"+
        "ctime() -- convert time in seconds to string\n"+
        "mktime() -- convert local time tuple to seconds since Epoch\n"+
        "strftime() -- convert time tuple to string according to "+
                "format specification\n"+
        "strptime() -- parse string to time tuple according to "+
                "format specification\n"
    );

    public static void classDictInit(PyObject dict) {
        dict.__setitem__("time", new TimeFunctions("time", 0, 0));
        dict.__setitem__("clock", new TimeFunctions("clock", 1, 0));
        dict.__setitem__("struct_time", PyTimeTuple.TYPE);
        dict.__setitem__("__name__", Py.newString("time"));

        // calculate the static variables tzname, timezone, altzone, daylight
        TimeZone tz = TimeZone.getDefault();

        tzname = new PyTuple(new PyString(tz.getDisplayName(false, 0)),
                             new PyString(tz.getDisplayName(true, 0)));

        daylight = tz.useDaylightTime() ? 1 : 0;
        timezone = -tz.getRawOffset() / 1000;
        altzone = timezone - tz.getDSTSavings() / 1000;
    }

    public static double time() {
        return System.currentTimeMillis()/1000.0;
    }

    /**
     * @return - the seconds elapsed since the first call to this function
     */
    public static double clock() {
        // Check against an explicit initialization variable, clockInitialized,
        // rather than a value of initialClock since the initial call to
        // System.nanoTime can yield anything and that could lead to initialTime
        // being set twice.
        if(!clockInitialized) {
            initialClock = System.nanoTime();
            clockInitialized = true;
            return 0;
        }
        return (System.nanoTime() - initialClock) / NANOS_PER_SECOND;
    }
    private static final double NANOS_PER_SECOND = 1000000000.0;
    private static long initialClock;
    private static volatile boolean clockInitialized;

    private static void throwValueError(String msg) {
        throw new PyException(Py.ValueError, new PyString(msg));
    }

    private static int item(PyTuple tup, int i) {
        // knows about and asserts format on tuple items.  See
        // documentation for Python's time module for details.
        int val = tup.__getitem__(i).asInt();
        boolean valid = true;
        switch (i) {
        case 0: break;                                  // year
        case 1: valid = (0 <= val && val <= 12); break; // month 1-12 (or 0)
        case 2: valid = (0 <= val && val <= 31); break; // day 1 - 31 (or 0)
        case 3: valid = (0 <= val && val <= 23); break; // hour 0 - 23
        case 4: valid = (0 <= val && val <= 59); break; // minute 0 - 59
        case 5: valid = (0 <= val && val <= 61); break; // second 0 - 59 (plus 2 leap seconds)
        case 6: valid = (0 <= val && val <= 6);  break; // weekday 0 - 6
        case 7: valid = (0 <= val && val < 367); break; // julian day 1 - 366 (or 0)
        case 8: valid = (-1 <= val && val <= 1); break; // d.s. flag, -1,0,1
        }
        // raise a ValueError if not within range
        if (!valid) {
            String msg;
            switch (i) {
            case 1:
                msg = "month out of range (1-12)";
                break;
            case 2:
                msg = "day out of range (1-31)";
                break;
            case 3:
                msg = "hour out of range (0-23)";
                break;
            case 4:
                msg = "minute out of range (0-59)";
                break;
            case 5:
                msg = "second out of range (0-59)";
                break;
            case 6:
                msg = "day of week out of range (0-6)";
                break;
            case 7:
                msg = "day of year out of range (1-366)";
                break;
            case 8:
                msg = "daylight savings flag out of range (-1,0,1)";
                break;
            default:
                // make compiler happy
                msg = "ignore";
                break;
            }
            throwValueError(msg);
        }
        switch (i) {
        case 1:
            if (val > 0) {
                // Java's months are usually 0-11
                val--;
            }
            break;
        case 2:
        case 7:
            // 'or 0'
            if (val == 0) {
                val = 1;
            }
            break;
        }
        return val;
    }

    private static GregorianCalendar _tupletocal(PyTuple tup) {
        return new GregorianCalendar(item(tup, 0),
                                     item(tup, 1),
                                     item(tup, 2),
                                     item(tup, 3),
                                     item(tup, 4),
                                     item(tup, 5));
    }

    public static double mktime(PyTuple tup) {
        GregorianCalendar cal;
        try {
            cal = _tupletocal(tup);
        }
        catch (PyException e) {
            // CPython's mktime raises OverflowErrors... yuck!
            e.type = Py.OverflowError;
            throw e;
        }
        int dst = item(tup, 8);
        if(dst == 0 || dst == 1) {
            cal.set(Calendar.DST_OFFSET, dst * cal.getTimeZone().getDSTSavings());
        }
        return cal.getTime().getTime()/1000.0;
    }

    protected static PyTimeTuple _timefields(double secs, TimeZone tz) {
        GregorianCalendar cal = new GregorianCalendar(tz);
        cal.clear();
        secs = secs * 1000;
        if (secs < Long.MIN_VALUE || secs > Long.MAX_VALUE) {
            throw Py.ValueError("timestamp out of range for platform time_t");
        }
        cal.setTime(new Date((long)secs));
        int isdst = tz.inDaylightTime(cal.getTime()) ? 1 : 0;
        return toTimeTuple(cal, isdst);
    }

    private static PyTimeTuple toTimeTuple(Calendar cal, int isdst) {
        int dow = cal.get(Calendar.DAY_OF_WEEK) - 2;
        if (dow < 0) {
            dow += 7;
        }
        return new PyTimeTuple(new PyInteger(cal.get(Calendar.YEAR)),
                new PyInteger(cal.get(Calendar.MONTH) + 1),
                new PyInteger(cal.get(Calendar.DAY_OF_MONTH)),
                new PyInteger(cal.get(Calendar.HOUR)
                + 12 * cal.get(Calendar.AM_PM)),
                new PyInteger(cal.get(Calendar.MINUTE)),
                new PyInteger(cal.get(Calendar.SECOND)),
                new PyInteger(dow),
                new PyInteger(cal.get(Calendar.DAY_OF_YEAR)),
                new PyInteger(isdst));
    }


    /**
     * Convert a time argument that may be an optional float or None value to a
     * double. Throws a TypeError on failure.
     *
     * @param arg a PyObject number of None
     * @return a double value
     */
    public static double parseTimeDoubleArg(PyObject arg) {
        if (arg == Py.None) {
            return time();
        }
        Object result = arg.__tojava__(Double.class);
        if (result == Py.NoConversion) {
            throw Py.TypeError("a float is required");
        }
        return (Double)result;
    }

    public static PyTuple localtime() {
        return localtime(Py.None);
    }

    public static PyTuple localtime(PyObject secs) {
        return _timefields(parseTimeDoubleArg(secs), TimeZone.getDefault());
    }

    public static PyTuple gmtime() {
        return gmtime(Py.None);
    }

    public static PyTuple gmtime(PyObject arg) {
        return _timefields(parseTimeDoubleArg(arg), TimeZone.getTimeZone("GMT"));
    }

    public static PyString ctime() {
        return ctime(Py.None);
    }

    public static PyString ctime(PyObject secs) {
        return asctime(localtime(secs));
    }

    // Python's time module specifies use of current locale
    protected static Locale currentLocale = null;
    protected static DateFormatSymbols datesyms = new DateFormatSymbols();
    protected static String[] shortdays = null;
    protected static String[] shortmonths = null;

    private static String[] enshortdays = new String[] {"Mon",
                                                        "Tue",
                                                        "Wed",
                                                        "Thu",
                                                        "Fri",
                                                        "Sat",
                                                        "Sun"};

    private static String[] enshortmonths = new String[] {"Jan",
                                                          "Feb",
                                                          "Mar",
                                                          "Apr",
                                                          "May",
                                                          "Jun",
                                                          "Jul",
                                                          "Aug",
                                                          "Sep",
                                                          "Oct",
                                                          "Nov",
                                                          "Dec"};

    private synchronized static String _shortday(int dow) {
        // we need to hand craft shortdays[] because Java and Python have
        // different specifications.  Java (undocumented) appears to be
        // first element "", followed by 0=Sun.  Python says 0=Mon
        if (shortdays == null) {
            shortdays = new String[7];
            String[] names = datesyms.getShortWeekdays();
            for (int i = 0; i < 6; i++) {
                shortdays[i] = names[i + 2];
            }
            shortdays[6] = names[1];
        }
        try {
            return shortdays[dow];
        } catch (ArrayIndexOutOfBoundsException e) {
            throw new PyException(Py.ValueError, new PyString("day of week out of range (0-6)"));
        }
    }

    private synchronized static String _shortmonth(int month0to11) {
        // getShortWeekdays() returns a 13 element array with the last item
        // being the empty string.  This is also undocumented ;-/
        if (shortmonths == null) {
            shortmonths = new String[12];
            String[] names = datesyms.getShortMonths();
            for (int i = 0; i < 12; i++) {
                shortmonths[i] = names[i];
            }
        }
        try {
            return shortmonths[month0to11];
        } catch (ArrayIndexOutOfBoundsException e) {
            throw new PyException(Py.ValueError, new PyString("month out of range (1-12)"));
        }
    }

    private static String _padint(int i, int target) {
        String s = Integer.toString(i);
        int sz = s.length();
        if (target <= sz)
            // no truncation
            return s;
        if (target == sz+1)
            return "0"+s;
        if (target == sz+2)
            return "00"+s;
        else {
            char[] c = new char[target-sz];
            Arrays.fill(c, '0');
            return new String(c) + s;
        }
    }

    private static String _twodigit(int i) {
        return _padint(i, 2);
    }

    private static String _truncyear(int year) {
        String yearstr = _padint(year, 4);
        return yearstr.substring(yearstr.length()-2, yearstr.length());
    }

    public static PyString asctime() {
        return asctime(localtime());
    }

    public static PyString asctime(PyTuple tup) {
        StringBuilder buf = new StringBuilder(25);
        buf.append(enshortdays[item(tup, 6)]).append(' ');
        buf.append(enshortmonths[item(tup, 1)]).append(' ');
        int dayOfMonth = item(tup, 2);
        if(dayOfMonth < 10){
            buf.append(' ');
        }
        buf.append(dayOfMonth).append(' ');
        buf.append(_twodigit(item(tup, 3))).append(':');
        buf.append(_twodigit(item(tup, 4))).append(':');
        buf.append(_twodigit(item(tup, 5))).append(' ');
        return new PyString(buf.append(item(tup, 0)).toString());
    }

    public static String locale_asctime(PyTuple tup) {
        checkLocale();
        int day = item(tup, 6);
        int mon = item(tup, 1);
        return _shortday(day) + " " + _shortmonth(mon) + " " +
            _twodigit(item(tup, 2)) + " " +
            _twodigit(item(tup, 3)) + ":" +
            _twodigit(item(tup, 4)) + ":" +
            _twodigit(item(tup, 5)) + " " +
            item(tup, 0);
    }

    public static void sleep(double secs) {
        try {
            java.lang.Thread.sleep((long)(secs * 1000));
        }
        catch (java.lang.InterruptedException e) {
            throw new PyException(Py.KeyboardInterrupt, "interrupted sleep");
        }
    }

    // set by classDictInit()
    public static int timezone;
    public static int altzone = -1;
    public static int daylight;
    public static PyTuple tzname = null;
    // TBD: should we accept 2 digit years?  should we make this attribute
    // writable but ignore its value?
    public static final int accept2dyear = 0;

    public static PyString strftime(String format) {
        return strftime(format, localtime());
    }

    public static PyString strftime(String format, PyTuple tup) {
        checkLocale();

        // Immediately validate the tuple
        int[] items = new int[9];
        for (int i = 0; i < 9; i ++) {
            items[i] = item(tup, i);
        }

        String s = "";
        int lastc = 0;
        int j;
        String[] syms;
        GregorianCalendar cal = null;
        while (lastc < format.length()) {
            int i = format.indexOf("%", lastc);
            if (i < 0) {
                // the end of the format string
                s = s + format.substring(lastc);
                break;
            }
            if (i == format.length() - 1) {
                // there's a bare % at the end of the string.  Python lets
                // this go by just sticking a % at the end of the result
                // string
                s = s + "%";
                break;
            }
            s = s + format.substring(lastc, i);
            i++;
            switch (format.charAt(i)) {
            case 'a':
                // abbrev weekday
                j = items[6];
                s = s + _shortday(j);
                break;
            case 'A':
                // full weekday
                // see _shortday()
                syms = datesyms.getWeekdays();
                j = items[6];
                if (0 <= j && j < 6)
                    s = s + syms[j+2];
                else if (j== 6)
                    s = s + syms[1];
                else
                    throwValueError("day of week out of range (0 - 6)");
                break;
            case 'b':
                // abbrev month
                j = items[1];
                s = s + _shortmonth(j);
                break;
            case 'B':
                // full month
                syms = datesyms.getMonths();
                j = items[1];
                s = s + syms[j];
                break;
            case 'c':
                s = s + locale_asctime(tup);
                break;
            case 'd':
                // day of month (01-31)
                s = s + _twodigit(items[2]);
                break;
            case 'H':
                // hour (00-23)
                s = s + _twodigit(items[3]);
                break;
            case 'I':
                // hour (01-12)
                j = items[3] % 12;
                if (j == 0)
                    j = 12;                  // midnight or noon
                s = s + _twodigit(j);
                break;
            case 'j':
                // day of year (001-366)
                s = s + _padint(items[7], 3);
                break;
            case 'm':
                // month (01-12)
                s = s + _twodigit(items[1] + 1);
                break;
            case 'M':
                // minute (00-59)
                s = s + _twodigit(items[4]);
                break;
            case 'p':
                // AM/PM
                j = items[3];
                syms = datesyms.getAmPmStrings();
                if (0 <= j && j < 12)
                    s = s + syms[0];
                else if (12 <= j && j < 24)
                    s = s + syms[1];
                else
                    throwValueError("hour out of range (0-23)");
                break;
            case 'S':
                // seconds (00-61)
                s = s + _twodigit(items[5]);
                break;
            case 'U':
                // week of year (sunday is first day) (00-53).  all days in
                // new year preceding first sunday are considered to be in
                // week 0
                if (cal == null)
                    cal = _tupletocal(tup);
                cal.setFirstDayOfWeek(Calendar.SUNDAY);
                cal.setMinimalDaysInFirstWeek(7);
                j = cal.get(Calendar.WEEK_OF_YEAR);
                if (cal.get(Calendar.MONTH) == Calendar.JANUARY && j >= 52)
                    j = 0;
                s = s + _twodigit(j);
                break;
            case 'w':
                // weekday as decimal (0=Sunday-6)
                // tuple format has monday=0
                j = (items[6] + 1) % 7;
                s = s + j;
                break;
            case 'W':
                // week of year (monday is first day) (00-53).  all days in
                // new year preceding first sunday are considered to be in
                // week 0
                if (cal == null)
                    cal = _tupletocal(tup);
                cal.setFirstDayOfWeek(Calendar.MONDAY);
                cal.setMinimalDaysInFirstWeek(7);
                j = cal.get(Calendar.WEEK_OF_YEAR);

                if (cal.get(Calendar.MONTH) == Calendar.JANUARY && j >= 52)
                    j = 0;
                s = s + _twodigit(j);
                break;
            case 'x':
                // TBD: A note about %x and %X.  Python's time.strftime()
                // by default uses the "C" locale, which is changed by
                // using the setlocale() function.  In Java, the default
                // locale is set by user.language and user.region
                // properties and is "en_US" by default, at least around
                // here!  Locale "en_US" differs from locale "C" in the way
                // it represents dates and times.  Eventually we might want
                // to craft a "C" locale for Java and set Jython to use
                // this by default, but that's too much work right now.
                //
                // For now, we hard code %x and %X to return values
                // formatted in the "C" locale, i.e. the default way
                // CPython does it.  E.g.:
                //     %x == mm/dd/yy
                //     %X == HH:mm:SS
                //
                s = s + _twodigit(items[1] + 1) + "/" +
                    _twodigit(items[2]) + "/" +
                    _truncyear(items[0]);
                break;
            case 'X':
                // See comment for %x above
                s = s + _twodigit(items[3]) + ":" +
                    _twodigit(items[4]) + ":" +
                    _twodigit(items[5]);
                break;
            case 'Y':
                // year w/ century
                s = s + _padint(items[0], 4);
                break;
            case 'y':
                // year w/o century (00-99)
                s = s + _truncyear(items[0]);
                break;
            case 'Z':
                // timezone name
                if (cal == null)
                    cal = _tupletocal(tup);
                // If items[8] == 1, we're in daylight savings time.
                // -1 means the information was not available; treat this as if not in dst.
                s = s + cal.getTimeZone().getDisplayName(items[8] > 0, 0);
                break;
            case '%':
                // %
                s = s + "%";
                break;
            default:
                // TBD: should this raise a ValueError?
                s = s + "%" + format.charAt(i);
                i++;
                break;
            }
            lastc = i+1;
            i++;
        }
        // FIXME: This have problems with localized data:
        //        $ LANG=es_ES.UTF-8 jythont -c "import time; print time.strftime('%A')"
        //        s?bado
        //
        //        On the other hand, returning unicode would break some doctests
        //        and will give problems when the return value of strftime is
        //        mixed with other strings and then the final result is stored
        //        on places which only support str and not unicode (such as
        //        os.environ)
        //
        // TODO:  Check how CPython deals with this problem.
        return new PyString(s);
    }


    private static void checkLocale() {
        if (!Locale.getDefault().equals(currentLocale)) {
            currentLocale = Locale.getDefault();
            datesyms = new DateFormatSymbols(currentLocale);
            shortdays = null;
            shortmonths = null;
        }
    }

    public static PyTuple strptime(String data_string) {
        return strptime(data_string, DEFAULT_FORMAT_PY);
    }

    /**
     * Calls _strptime.strptime(), for cases that our SimpleDateFormat backed
     * strptime can't handle.
     */
    private static PyTuple pystrptime(String data_string, String format) {
        return (PyTuple) __builtin__.__import__("_strptime")
                                    .__getattr__("strptime")
                                    .__call__(Py.newUnicode(data_string),
                                              Py.newUnicode(format));
    }

    public static PyTuple strptime(String data_string, String format) {
        if (format == null || data_string == null) {
            // this is not a very interesting error message, but it's the same
            // as what CPython outputs
            throw Py.TypeError("expected string of buffer");
        }
        String jformat = py2java_format(format);
        if (jformat == null) {
            // Format not translatable to java, fallback to _strptime
            return pystrptime(data_string, format);
        }
        SimpleDateFormat d = new SimpleDateFormat(jformat);
        Calendar cal = Calendar.getInstance();
        try {
            cal.setTime(d.parse(data_string));
        } catch (ParseException e) {
            throwValueError("time data did not match format:  data=" + data_string + "  fmt=" + format);
        }
        int isdst = -1;
        if (jformat.contains("zzz")) {
            isdst = cal.getTimeZone().inDaylightTime(cal.getTime()) ? 1 : 0;
        }
        return toTimeTuple(cal, isdst);
    }

    private static final String DEFAULT_FORMAT_PY = "%a %b %d %H:%M:%S %Y";
    private static final HashMap<Character, String> py2java = new HashMap<Character, String>() {{
        put('a', "EEE");
        put('A', "EEEE");
        put('b', "MMM");
        put('B', "MMMM");
        put('c', "EEE MMM dd HH:mm:ss yyyy");
        put('d', "dd");
        put('H', "HH");
        put('I', "hh");
        put('j', "DDD");
        put('m', "MM");
        put('M', "mm");
        put('p', "a");
        put('S', "ss");
        put('U', "ww");
        put('W', "ww"); // same as U ??
        put('x', "MM/dd/yy");
        put('X', "HH:mm:ss");
        put('y', "yy");
        put('Y', "yyyy");
        put('Z', "zzz");
        put('%', "%");
    }};

    // strptime formats not supported by SimpleDateFormat:
    private static final List<Character> notSupported = new ArrayList<Character>() {{
        add('w');
    }};


    /**
     * @return the {@link SimpleDateFormat} format string equivalent to the
     * strptime format string supplied as parameter, or <b>null</b> if there is
     * no equivalent for SimpleDateFormat.
     */
    private static String py2java_format(String format) {
        StringBuilder builder = new StringBuilder();
        boolean directive = false;
        boolean inQuote = false;

        if (format.length() == 0) {
            return null;
        }

        for (int i = 0; i < format.length(); i++) {
            char charAt = format.charAt(i);

            if (charAt == '%' && !directive) {
                directive = true;
                continue;
            }

            if (!directive) {
                // ascii letters are considered SimpleDateFormat directive patterns unless
                // escaped
                boolean needsQuote = (charAt >= 'A' && charAt <= 'Z') ||
                        (charAt >= 'a' && charAt <= 'z');
                if (needsQuote && !inQuote || !needsQuote && inQuote) {
                    builder.append("'");
                    inQuote = needsQuote;
                }
                builder.append(charAt);
                continue;
            } else if (inQuote) {
                builder.append("'");
                inQuote = false;
            }

            String translated = py2java.get(charAt);
            if (translated == null && notSupported.contains(charAt)) {
                return null;
            }
            builder.append(translated != null ? translated : charAt);
            directive = false;
        }
        if (inQuote) {
            builder.append("'");
        }
        return builder.toString();
    }
}
