package java.util;
import java.io.Serializable;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.time.ZoneId;
import sun.security.action.GetPropertyAction;
import sun.util.calendar.ZoneInfo;
import sun.util.calendar.ZoneInfoFile;
import sun.util.locale.provider.TimeZoneNameUtility;

//抽象类: 时区
abstract public class TimeZone implements Serializable, Cloneable {

    public TimeZone(){
    }

    //时间显示风格 简短的表示时间
    public static final int SHORT = 0;

    //时间显示风格,完整的表示时间
    public static final int LONG = 1;

    // Constants used internally; unit is milliseconds,时间常量转换为整型
    private static final int ONE_MINUTE = 60 * 1000;
    private static final int ONE_HOUR = 60 * ONE_MINUTE;
    private static final int ONE_DAY = 24 * ONE_HOUR;

    // Proclaim serialization compatibility with JDK 1.1
    static final long serialVersionUID = 3581463369166924961L;

    //获取当前日期的时区偏移,在夏令时情况下进行修改。
    public abstract int getOffset(int era, int year, int month, int day,
            int dayOfWeek, int milliseconds);

    //在指定的日期返回此时区与UTC的偏移量。
    public int getOffset(long date){
        if(inDaylightTime(new Date(date))){
            return getRawOffset() + getDSTSavings();
        }
        return getRawOffset();
    }

    //给定时间的原始GMT偏移量和夏令时时间,获取UTC偏移量
    int getOffsets(long date, int[] offsets){
        int rawoffset = getRawOffset();
        int dstoffset = 0;
        if(inDaylightTime(new Date(date))){
            dstoffset = getDSTSavings();
        }
        if(offsets != null){
            offsets[0] = rawoffset;
            offsets[1] = dstoffset;
        }
        return rawoffset + dstoffset;
    }

    //将基准时区偏移设置为GMT。
    abstract public void setRawOffset(int offsetMillis);

    //返回添加到UTC的时间(以毫秒为单位),以获得此时区的标准时间。 
    public abstract int getRawOffset();

    public String getID(){
        return ID;
    }

    //    设置时区ID。 
    public void setID(String ID){
        if(ID == null){
            throw new NullPointerException();
        }
        this.ID = ID;
    }


    public final String getDisplayName(){
        return getDisplayName(false, LONG,
                              Locale.getDefault(Locale.Category.DISPLAY));
    }

    public final String getDisplayName(Locale locale){
        return getDisplayName(false, LONG, locale);
    }

    public final String getDisplayName(boolean daylight, int style){
        return getDisplayName(daylight, style,
                              Locale.getDefault(Locale.Category.DISPLAY));
    }

    public String getDisplayName(boolean daylight, int style, Locale locale){
        if(style != SHORT && style != LONG){
            throw new IllegalArgumentException("Illegal style: " + style);
        }
        String id = getID();
        String name = TimeZoneNameUtility.retrieveDisplayName(id, daylight, style, locale);
        if(name != null){
            return name;
        }

        if(id.startsWith("GMT") && id.length() > 3){
            char sign = id.charAt(3);
            if(sign == '+' || sign == '-'){
                return id;
            }
        }
        int offset = getRawOffset();
        if(daylight){
            offset += getDSTSavings();
        }
        return ZoneInfoFile.toCustomID(offset);
    }

    private static String[] getDisplayNames(String id, Locale locale){
        return TimeZoneNameUtility.retrieveDisplayNames(id, locale);
    }

    public int getDSTSavings(){
        if(useDaylightTime()){
            return 3600000;
        }
        return 0;
    }

    public abstract boolean useDaylightTime();

    public boolean observesDaylightTime(){
        return useDaylightTime() || inDaylightTime(new Date());
    }

    abstract public boolean inDaylightTime(Date date);

    public static synchronized TimeZone getTimeZone(String ID){
        return getTimeZone(ID, true);
    }

    public static TimeZone getTimeZone(ZoneId zoneId){
        String tzid = zoneId.getId(); // throws an NPE if null
        char c = tzid.charAt(0);
        if(c == '+' || c == '-'){
            tzid = "GMT" + tzid;
        }else if(c == 'Z' && tzid.length() == 1){
            tzid = "UTC";
        }
        return getTimeZone(tzid, true);
    }

    public ZoneId toZoneId(){
        String id = getID();
        if(ZoneInfoFile.useOldMapping() && id.length() == 3){
            if("EST".equals(id))
                return ZoneId.of("America/New_York");
            if("MST".equals(id))
                return ZoneId.of("America/Denver");
            if("HST".equals(id))
                return ZoneId.of("America/Honolulu");
        }
        return ZoneId.of(id, ZoneId.SHORT_IDS);
    }

    private static TimeZone getTimeZone(String ID, boolean fallback){
        TimeZone tz = ZoneInfo.getTimeZone(ID);
        if(tz == null){
            tz = parseCustomTimeZone(ID);
            if(tz == null && fallback){
                tz = new ZoneInfo(GMT_ID, 0);
            }
        }
        return tz;
    }

    public static synchronized String[] getAvailableIDs(int rawOffset){
        return ZoneInfo.getAvailableIDs(rawOffset);
    }

    public static synchronized String[] getAvailableIDs(){
        return ZoneInfo.getAvailableIDs();
    }

    private static native String getSystemTimeZoneID(String javaHome);

    private static native String getSystemGMTOffsetID();

    public static TimeZone getDefault(){
        return (TimeZone) getDefaultRef().clone();
    }

    static TimeZone getDefaultRef(){
        TimeZone defaultZone = defaultTimeZone;
        if(defaultZone == null){
            // Need to initialize the default time zone.
            defaultZone = setDefaultZone();
            assert defaultZone != null;
        }
        // Don't clone here.
        return defaultZone;
    }

    private static synchronized TimeZone setDefaultZone(){
        TimeZone tz;
        // get the time zone ID from the system properties
        String zoneID = AccessController.doPrivileged(
                new GetPropertyAction("user.timezone"));

        // if the time zone ID is not set (yet), perform the
        // platform to Java time zone ID mapping.
        if(zoneID == null || zoneID.isEmpty()){
            String javaHome = AccessController.doPrivileged(
                    new GetPropertyAction("java.home"));
            try {
                zoneID = getSystemTimeZoneID(javaHome);
                if(zoneID == null){
                    zoneID = GMT_ID;
                }
            } catch (NullPointerException e) {
                zoneID = GMT_ID;
            }
        }

        // Get the time zone for zoneID. But not fall back to
        // "GMT" here.
        tz = getTimeZone(zoneID, false);

        if(tz == null){
            // If the given zone ID is unknown in Java, try to
            // get the GMT-offset-based time zone ID,
            // a.k.a. custom time zone ID (e.g., "GMT-08:00").
            String gmtOffsetID = getSystemGMTOffsetID();
            if(gmtOffsetID != null){
                zoneID = gmtOffsetID;
            }
            tz = getTimeZone(zoneID, true);
        }
        assert tz != null;

        final String id = zoneID;
        AccessController.doPrivileged(new PrivilegedAction<Void>() {
            @Override
            public Void run(){
                System.setProperty("user.timezone", id);
                return null;
            }
        });

        defaultTimeZone = tz;
        return tz;
    }

    public static void setDefault(TimeZone zone){
        SecurityManager sm = System.getSecurityManager();
        if(sm != null){
            sm.checkPermission(new PropertyPermission
                                       ("user.timezone", "write"));
        }
        defaultTimeZone = zone;
    }

    public boolean hasSameRules(TimeZone other){
        return other != null && getRawOffset() == other.getRawOffset() &&
               useDaylightTime() == other.useDaylightTime();
    }

    public Object clone(){
        try {
            TimeZone other = (TimeZone) super.clone();
            other.ID = ID;
            return other;
        } catch (CloneNotSupportedException e) {
            throw new InternalError(e);
        }
    }


    static final TimeZone NO_TIMEZONE = null;

    // =======================privates===============================

    private String ID;
    private static volatile TimeZone defaultTimeZone;

    static final String GMT_ID = "GMT";
    private static final int GMT_ID_LENGTH = 3;

    private static volatile TimeZone mainAppContextDefault;

    private static final TimeZone parseCustomTimeZone(String id){
        int length;

        // Error if the length of id isn't long enough or id doesn't
        // start with "GMT".
        if((length = id.length()) < (GMT_ID_LENGTH + 2) ||
           id.indexOf(GMT_ID) != 0){
            return null;
        }

        ZoneInfo zi;

        // First, we try to find it in the cache with the given
        // id. Even the id is not normalized, the returned ZoneInfo
        // should have its normalized id.
        zi = ZoneInfoFile.getZoneInfo(id);
        if(zi != null){
            return zi;
        }

        int index = GMT_ID_LENGTH;
        boolean negative = false;
        char c = id.charAt(index++);
        if(c == '-'){
            negative = true;
        }else if(c != '+'){
            return null;
        }

        int hours = 0;
        int num = 0;
        int countDelim = 0;
        int len = 0;
        while (index < length) {
            c = id.charAt(index++);
            if(c == ':'){
                if(countDelim > 0){
                    return null;
                }
                if(len > 2){
                    return null;
                }
                hours = num;
                countDelim++;
                num = 0;
                len = 0;
                continue;
            }
            if(c < '0' || c > '9'){
                return null;
            }
            num = num * 10 + (c - '0');
            len++;
        }
        if(index != length){
            return null;
        }
        if(countDelim == 0){
            if(len <= 2){
                hours = num;
                num = 0;
            }else{
                hours = num / 100;
                num %= 100;
            }
        }else{
            if(len != 2){
                return null;
            }
        }
        if(hours > 23 || num > 59){
            return null;
        }
        int gmtOffset = (hours * 60 + num) * 60 * 1000;

        if(gmtOffset == 0){
            zi = ZoneInfoFile.getZoneInfo(GMT_ID);
            if(negative){
                zi.setID("GMT-00:00");
            }else{
                zi.setID("GMT+00:00");
            }
        }else{
            zi = ZoneInfoFile.getCustomTimeZone(id, negative ? -gmtOffset : gmtOffset);
        }
        return zi;
    }
}