/*
 *  Copyright 2001-2012 Stephen Colebourne
 *
 *  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.joda.time;

import java.lang.reflect.Method;
import java.text.DateFormatSymbols;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Locale;
import java.util.Map;

import org.joda.time.chrono.ISOChronology;

/**
 * DateTimeUtils provide public utility methods for the date-time library.
 * <p>
 * DateTimeUtils is thread-safe although shared static variables are used.
 *
 * @author Stephen Colebourne
 * @since 1.0
 */
public class DateTimeUtils {

    /** The singleton instance of the system millisecond provider. */
    private static final SystemMillisProvider SYSTEM_MILLIS_PROVIDER = new SystemMillisProvider();
    /** The millisecond provider currently in use. */
    private static volatile MillisProvider cMillisProvider = SYSTEM_MILLIS_PROVIDER;
    /** The millisecond provider currently in use. */
    private static volatile Map<String, DateTimeZone> cZoneNames;
    static {
        // names from RFC-822 / JDK
        // this is all very US-centric and dubious, but perhaps it will help some
        Map<String, DateTimeZone> map = new LinkedHashMap<String, DateTimeZone>();
        map.put("UT", DateTimeZone.UTC);
        map.put("UTC", DateTimeZone.UTC);
        map.put("GMT", DateTimeZone.UTC);
        put(map, "EST", "America/New_York");
        put(map, "EDT", "America/New_York");
        put(map, "CST", "America/Chicago");
        put(map, "CDT", "America/Chicago");
        put(map, "MST", "America/Denver");
        put(map, "MDT", "America/Denver");
        put(map, "PST", "America/Los_Angeles");
        put(map, "PDT", "America/Los_Angeles");
        cZoneNames = Collections.unmodifiableMap(map);
    }
    private static void put(Map<String, DateTimeZone> map, String name, String id) {
        try {
            map.put(name, DateTimeZone.forID(id));
        } catch (RuntimeException ex) {
            // ignore
        }
    }

    /**
     * Restrictive constructor
     */
    protected DateTimeUtils() {
        super();
    }

    //-----------------------------------------------------------------------
    /**
     * Gets the current time in milliseconds.
     * <p>
     * By default this returns <code>System.currentTimeMillis()</code>.
     * This may be changed using other methods in this class.
     * 
     * @return the current time in milliseconds from 1970-01-01T00:00:00Z
     */
    public static final long currentTimeMillis() {
        return cMillisProvider.getMillis();
    }

    /**
     * Resets the current time to return the system time.
     * <p>
     * This method changes the behaviour of {@link #currentTimeMillis()}.
     * Whenever the current time is queried, {@link System#currentTimeMillis()} is used.
     * 
     * @throws SecurityException if the application does not have sufficient security rights
     */
    public static final void setCurrentMillisSystem() throws SecurityException {
        checkPermission();
        cMillisProvider = SYSTEM_MILLIS_PROVIDER;
    }

    /**
     * Sets the current time to return a fixed millisecond time.
     * <p>
     * This method changes the behaviour of {@link #currentTimeMillis()}.
     * Whenever the current time is queried, the same millisecond time will be returned.
     * 
     * @param fixedMillis  the fixed millisecond time to use
     * @throws SecurityException if the application does not have sufficient security rights
     */
    public static final void setCurrentMillisFixed(long fixedMillis) throws SecurityException {
        checkPermission();
        cMillisProvider = new FixedMillisProvider(fixedMillis);
    }

    /**
     * Sets the current time to return the system time plus an offset.
     * <p>
     * This method changes the behaviour of {@link #currentTimeMillis()}.
     * Whenever the current time is queried, {@link System#currentTimeMillis()} is used
     * and then offset by adding the millisecond value specified here.
     * 
     * @param offsetMillis  the fixed millisecond time to use
     * @throws SecurityException if the application does not have sufficient security rights
     */
    public static final void setCurrentMillisOffset(long offsetMillis) throws SecurityException {
        checkPermission();
        if (offsetMillis == 0) {
            cMillisProvider = SYSTEM_MILLIS_PROVIDER;
        } else {
            cMillisProvider = new OffsetMillisProvider(offsetMillis);
        }
    }

    /**
     * Sets the provider of the current time to class specified.
     * <p>
     * This method changes the behaviour of {@link #currentTimeMillis()}.
     * Whenever the current time is queried, the specified class will be called.
     * 
     * @param millisProvider  the provider of the current time to use, not null
     * @throws SecurityException if the application does not have sufficient security rights
     * @since 2.0
     */
    public static final void setCurrentMillisProvider(MillisProvider millisProvider) throws SecurityException {
        if (millisProvider == null) {
            throw new IllegalArgumentException("The MillisProvider must not be null");
        }
        checkPermission();
        cMillisProvider = millisProvider;
    }

    /**
     * Checks whether the provider may be changed using permission 'CurrentTime.setProvider'.
     * 
     * @throws SecurityException if the provider may not be changed
     */
    private static void checkPermission() throws SecurityException {
        SecurityManager sm = System.getSecurityManager();
        if (sm != null) {
            sm.checkPermission(new JodaTimePermission("CurrentTime.setProvider"));
        }
    }

    //-----------------------------------------------------------------------
    /**
     * Gets the millisecond instant from the specified instant object handling null.
     * <p>
     * If the instant object is <code>null</code>, the {@link #currentTimeMillis()}
     * will be returned. Otherwise, the millis from the object are returned.
     * 
     * @param instant  the instant to examine, null means now
     * @return the time in milliseconds from 1970-01-01T00:00:00Z
     */
    public static final long getInstantMillis(ReadableInstant instant) {
        if (instant == null) {
            return DateTimeUtils.currentTimeMillis();
        }
        return instant.getMillis();
    }

    //-----------------------------------------------------------------------
    /**
     * Gets the chronology from the specified instant object handling null.
     * <p>
     * If the instant object is <code>null</code>, or the instant's chronology is
     * <code>null</code>, {@link ISOChronology#getInstance()} will be returned.
     * Otherwise, the chronology from the object is returned.
     * 
     * @param instant  the instant to examine, null means ISO in the default zone
     * @return the chronology, never null
     */
    public static final Chronology getInstantChronology(ReadableInstant instant) {
        if (instant == null) {
            return ISOChronology.getInstance();
        }
        Chronology chrono = instant.getChronology();
        if (chrono == null) {
            return ISOChronology.getInstance();
        }
        return chrono;
    }

    //-----------------------------------------------------------------------
    /**
     * Gets the chronology from the specified instant based interval handling null.
     * <p>
     * The chronology is obtained from the start if that is not null, or from the
     * end if the start is null. The result is additionally checked, and if still
     * null then {@link ISOChronology#getInstance()} will be returned.
     * 
     * @param start  the instant to examine and use as the primary source of the chronology
     * @param end  the instant to examine and use as the secondary source of the chronology
     * @return the chronology, never null
     */
    public static final Chronology getIntervalChronology(ReadableInstant start, ReadableInstant end) {
        Chronology chrono = null;
        if (start != null) {
            chrono = start.getChronology();
        } else if (end != null) {
            chrono = end.getChronology();
        }
        if (chrono == null) {
            chrono = ISOChronology.getInstance();
        }
        return chrono;
    }

    //-----------------------------------------------------------------------
    /**
     * Gets the chronology from the specified interval object handling null.
     * <p>
     * If the interval object is <code>null</code>, or the interval's chronology is
     * <code>null</code>, {@link ISOChronology#getInstance()} will be returned.
     * Otherwise, the chronology from the object is returned.
     * 
     * @param interval  the interval to examine, null means ISO in the default zone
     * @return the chronology, never null
     */
    public static final Chronology getIntervalChronology(ReadableInterval interval) {
        if (interval == null) {
            return ISOChronology.getInstance();
        }
        Chronology chrono = interval.getChronology();
        if (chrono == null) {
            return ISOChronology.getInstance();
        }
        return chrono;
    }

    //-----------------------------------------------------------------------
    /**
     * Gets the interval handling null.
     * <p>
     * If the interval is <code>null</code>, an interval representing now
     * to now in the {@link ISOChronology#getInstance() ISOChronology}
     * will be returned. Otherwise, the interval specified is returned.
     * 
     * @param interval  the interval to use, null means now to now
     * @return the interval, never null
     * @since 1.1
     */
    public static final ReadableInterval getReadableInterval(ReadableInterval interval) {
        if (interval == null) {
            long now = DateTimeUtils.currentTimeMillis();
            interval = new Interval(now, now);
        }
        return interval;
    }

    //-----------------------------------------------------------------------
    /**
     * Gets the chronology handling null.
     * <p>
     * If the chronology is <code>null</code>, {@link ISOChronology#getInstance()}
     * will be returned. Otherwise, the chronology is returned.
     * 
     * @param chrono  the chronology to use, null means ISO in the default zone
     * @return the chronology, never null
     */
    public static final Chronology getChronology(Chronology chrono) {
        if (chrono == null) {
            return ISOChronology.getInstance();
        }
        return chrono;
    }

    //-----------------------------------------------------------------------
    /**
     * Gets the zone handling null.
     * <p>
     * If the zone is <code>null</code>, {@link DateTimeZone#getDefault()}
     * will be returned. Otherwise, the zone specified is returned.
     * 
     * @param zone  the time zone to use, null means the default zone
     * @return the time zone, never null
     */
    public static final DateTimeZone getZone(DateTimeZone zone) {
        if (zone == null) {
            return DateTimeZone.getDefault();
        }
        return zone;
    }

    //-----------------------------------------------------------------------
    /**
     * Gets the period type handling null.
     * <p>
     * If the zone is <code>null</code>, {@link PeriodType#standard()}
     * will be returned. Otherwise, the type specified is returned.
     * 
     * @param type  the time zone to use, null means the standard type
     * @return the type to use, never null
     */
    public static final PeriodType getPeriodType(PeriodType type) {
        if (type == null) {
            return PeriodType.standard();
        }
        return type;
    }

    //-----------------------------------------------------------------------
    /**
     * Gets the millisecond duration from the specified duration object handling null.
     * <p>
     * If the duration object is <code>null</code>, zero will be returned.
     * Otherwise, the millis from the object are returned.
     * 
     * @param duration  the duration to examine, null means zero
     * @return the duration in milliseconds
     */
    public static final long getDurationMillis(ReadableDuration duration) {
        if (duration == null) {
            return 0L;
        }
        return duration.getMillis();
    }

    //-----------------------------------------------------------------------
    /**
     * Checks whether the partial is contiguous.
     * <p>
     * A partial is contiguous if one field starts where another ends.
     * <p>
     * For example <code>LocalDate</code> is contiguous because DayOfMonth has
     * the same range (Month) as the unit of the next field (MonthOfYear), and
     * MonthOfYear has the same range (Year) as the unit of the next field (Year).
     * <p>
     * Similarly, <code>LocalTime</code> is contiguous, as it consists of
     * MillisOfSecond, SecondOfMinute, MinuteOfHour and HourOfDay (note how
     * the names of each field 'join up').
     * <p>
     * However, a Year/HourOfDay partial is not contiguous because the range
     * field Day is not equal to the next field Year.
     * Similarly, a DayOfWeek/DayOfMonth partial is not contiguous because
     * the range Month is not equal to the next field Day.
     * 
     * @param partial  the partial to check
     * @return true if the partial is contiguous
     * @throws IllegalArgumentException if the partial is null
     * @since 1.1
     */
    public static final boolean isContiguous(ReadablePartial partial) {
        if (partial == null) {
            throw new IllegalArgumentException("Partial must not be null");
        }
        DurationFieldType lastType = null;
        for (int i = 0; i < partial.size(); i++) {
            DateTimeField loopField = partial.getField(i);
            if (i > 0) {
                if (loopField.getRangeDurationField().getType() != lastType) {
                    return false;
                }
            }
            lastType = loopField.getDurationField().getType();
        }
        return true;
    }

    //-----------------------------------------------------------------------
    /**
     * Gets the {@link DateFormatSymbols} based on the given locale.
     * <p>
     * If JDK 6 or newer is being used, DateFormatSymbols.getInstance(locale) will
     * be used in order to allow the use of locales defined as extensions.
     * Otherwise, new DateFormatSymbols(locale) will be used.
     * See JDK 6 {@link DateFormatSymbols} for further information.
     * 
     * @param locale  the {@link Locale} used to get the correct {@link DateFormatSymbols}
     * @return the symbols
     * @since 2.0
     */
    public static final DateFormatSymbols getDateFormatSymbols(Locale locale) {
        try {
            Method method = DateFormatSymbols.class.getMethod("getInstance", new Class[] {Locale.class});
            return (DateFormatSymbols) method.invoke(null, new Object[] {locale});
        } catch (Exception ex) {
            return new DateFormatSymbols(locale);
        }
    }

    //-----------------------------------------------------------------------
    /**
     * Gets the default map of time zone names.
     * <p>
     * This can be changed by {@link #setDefaultTimeZoneNames}.
     * <p>
     * The default set of short time zone names is as follows:
     * <ul>
     * <li>UT - UTC
     * <li>UTC - UTC
     * <li>GMT - UTC
     * <li>EST - America/New_York
     * <li>EDT - America/New_York
     * <li>CST - America/Chicago
     * <li>CDT - America/Chicago
     * <li>MST - America/Denver
     * <li>MDT - America/Denver
     * <li>PST - America/Los_Angeles
     * <li>PDT - America/Los_Angeles
     * </ul>
     * 
     * @return the unmodifiable map of abbreviations to zones, not null
     * @since 2.2
     */
    public static final Map<String, DateTimeZone> getDefaultTimeZoneNames() {
        return cZoneNames;
    }

    /**
     * Sets the default map of time zone names.
     * <p>
     * The map is copied before storage.
     * 
     * @param names  the map of abbreviations to zones, not null
     * @since 2.2
     */
    public static final void setDefaultTimeZoneNames(Map<String, DateTimeZone> names) {
        cZoneNames = Collections.unmodifiableMap(new HashMap<String, DateTimeZone>(names));
    }

    //-------------------------------------------------------------------------
    /**
     * Calculates the astronomical Julian Day for an instant.
     * <p>
     * The <a href="http://en.wikipedia.org/wiki/Julian_day">Julian day</a> is a well-known
     * system of time measurement for scientific use by the astronomy community.
     * It expresses the interval of time in days and fractions of a day since
     * January 1, 4713 BC (Julian) Greenwich noon.
     * <p>
     * Each day starts at midday (not midnight) and time is expressed as a fraction.
     * Thus the fraction 0.25 is 18:00. equal to one quarter of the day from midday to midday.
     * <p>
     * Note that this method has nothing to do with the day-of-year.
     * 
     * @param epochMillis  the epoch millis from 1970-01-01Z
     * @return the astronomical Julian Day represented by the specified instant
     * @since 2.2
     */
    public static final double toJulianDay(long epochMillis) {
        // useful links
        // http://en.wikipedia.org/wiki/Julian_day#cite_note-13 - Wikipedia
        // http://aa.usno.navy.mil/data/docs/JulianDate.php" - USNO
        // http://users.zoominternet.net/~matto/Java/Julian%20Date%20Converter.htm - Julian Date Converter by Matt Oltersdorf
        // http://ssd.jpl.nasa.gov/tc.cgi#top - CalTech
        double epochDay = epochMillis / 86400000d;
        return epochDay + 2440587.5d;
    }

    /**
     * Calculates the astronomical Julian Day Number for an instant.
     * <p>
     * The {@link #toJulianDay(long)} method calculates the astronomical Julian Day
     * with a fraction based on days starting at midday.
     * This method calculates the variant where days start at midnight.
     * JDN 0 is used for the date equivalent to Monday January 1, 4713 BC (Julian).
     * Thus these days start 12 hours before those of the fractional Julian Day.
     * <p>
     * Note that this method has nothing to do with the day-of-year.
     * 
     * @param epochMillis  the epoch millis from 1970-01-01Z
     * @return the astronomical Julian Day represented by the specified instant
     * @since 2.2
     */
    public static final long toJulianDayNumber(long epochMillis) {
        return (long) Math.floor(toJulianDay(epochMillis) + 0.5d);
    }

    /**
     * Creates a date-time from a Julian Day.
     * <p>
     * Returns the {@code DateTime} object equal to the specified Julian Day.
     * 
     * @param julianDay  the Julian Day
     * @return the epoch millis from 1970-01-01Z
     * @since 2.2
     */
    public static final long fromJulianDay(double julianDay) {
        double epochDay = julianDay - 2440587.5d;
        return (long) (epochDay * 86400000d);
    }

    //-----------------------------------------------------------------------
    /**
     * A millisecond provider, allowing control of the system clock.
     * 
     * @author Stephen Colebourne
     * @since 2.0 (previously private)
     */
    public static interface MillisProvider {
        /**
         * Gets the current time.
         * <p>
         * Implementations of this method must be thread-safe.
         * 
         * @return the current time in milliseconds
         */
        long getMillis();
    }

    /**
     * System millis provider.
     */
    static class SystemMillisProvider implements MillisProvider {
        /**
         * Gets the current time.
         * @return the current time in millis
         */
        public long getMillis() {
            return System.currentTimeMillis();
        }
    }

    /**
     * Fixed millisecond provider.
     */
    static class FixedMillisProvider implements MillisProvider {
        /** The fixed millis value. */
        private final long iMillis;
        
        /**
         * Constructor.
         * @param offsetMillis  the millis offset
         */
        FixedMillisProvider(long fixedMillis) {
            iMillis = fixedMillis;
        }
        
        /**
         * Gets the current time.
         * @return the current time in millis
         */
        public long getMillis() {
            return iMillis;
        }
    }

    /**
     * Offset from system millis provider.
     */
    static class OffsetMillisProvider implements MillisProvider {
        /** The millis offset. */
        private final long iMillis;
        
        /**
         * Constructor.
         * @param offsetMillis  the millis offset
         */
        OffsetMillisProvider(long offsetMillis) {
            iMillis = offsetMillis;
        }
        
        /**
         * Gets the current time.
         * @return the current time in millis
         */
        public long getMillis() {
            return System.currentTimeMillis() + iMillis;
        }
    }

}
