package java.util;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.OptionalDataException;
import java.io.Serializable;
import java.security.AccessControlContext;
import java.security.AccessController;
import java.security.PermissionCollection;
import java.security.PrivilegedActionException;
import java.security.PrivilegedExceptionAction;
import java.security.ProtectionDomain;
import java.text.DateFormat;
import java.text.DateFormatSymbols;
import java.time.Instant;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

import sun.util.BuddhistCalendar;
import sun.util.calendar.ZoneInfo;
import sun.util.locale.provider.CalendarDataUtility;
import sun.util.locale.provider.LocaleProviderAdapter;
import sun.util.spi.CalendarProvider;

//抽象类:实现了Serializable, Cloneable, Comparable接口
public abstract class Calendar implements Serializable, Cloneable, Comparable<Calendar> {

    // Data flow in Calendar,时间量的转换
    //   local fields (YEAR, MONTH, DATE, HOUR, MINUTE, etc.)
    //           |
    //           |Using Calendar-specific algorithm
    //           V
    //   local standard millis
    //           |
    //           | Using TimeZone or user-set ZONE_OFFSET / DST_OFFSET
    //           V
    //   UTC millis (in time data member)


    public final static int ERA = 0;   //纪元
    public final static int YEAR = 1;  //年
    public final static int MONTH = 2; //月份,JANUARY为0

    public final static int WEEK_OF_YEAR = 3; //一年中第几周
    public final static int WEEK_OF_MONTH = 4;//月份中的第几周
    public final static int DATE = 5; //本月份中的第几天,日期
    public final static int DAY_OF_MONTH = 5;//本月中的第几天,日期
    public final static int DAY_OF_YEAR = 6;//本年的第几天
    public final static int DAY_OF_WEEK = 7;//week第几天,星期几
    public final static int DAY_OF_WEEK_IN_MONTH = 8;//本月内星期几的序号
    public final static int AM_PM = 9; //时间是AM/PM
    public final static int HOUR = 10; //小时 12小时制
    public final static int HOUR_OF_DAY = 11;//小时 24小时制
    public final static int MINUTE = 12; //分钟
    public final static int SECOND = 13;//秒
    public final static int MILLISECOND = 14;//毫秒
    public final static int ZONE_OFFSET = 15; //GMT的原始偏移量(毫秒)
    public final static int DST_OFFSET = 16;//一天的毫秒偏移量
    public final static int FIELD_COUNT = 17;//属性编号值
    //星期
    public final static int SUNDAY = 1;
    public final static int MONDAY = 2;
    public final static int TUESDAY = 3;
    public final static int WEDNESDAY = 4;
    public final static int THURSDAY = 5;
    public final static int FRIDAY = 6;
    public final static int SATURDAY = 7;
    //月份
    public final static int JANUARY = 0;
    public final static int FEBRUARY = 1;
    public final static int MARCH = 2;
    public final static int APRIL = 3;
    public final static int MAY = 4;
    public final static int JUNE = 5;
    public final static int JULY = 6;
    public final static int AUGUST = 7;
    public final static int SEPTEMBER = 8;
    public final static int OCTOBER = 9;
    public final static int NOVEMBER = 10;
    public final static int DECEMBER = 11;
    public final static int UNDECIMBER = 12; //第13个月
    //上下午
    public final static int AM = 0;
    public final static int PM = 1;

    //时间显示风格
    public static final int ALL_STYLES = 0;

    static final int STANDALONE_MASK = 0x8000;

    //时间显示风格
    public static final int SHORT = 1;
    public static final int LONG = 2;
    public static final int NARROW_FORMAT = 4;
    public static final int NARROW_STANDALONE = NARROW_FORMAT | STANDALONE_MASK;
    public static final int SHORT_FORMAT = 1;

    public static final int LONG_FORMAT = 2;

    public static final int SHORT_STANDALONE = SHORT | STANDALONE_MASK;

    public static final int LONG_STANDALONE = LONG | STANDALONE_MASK;

    protected int fields[];     //Calendar类值都存在这个数组中
    protected boolean isSet[];  //标记每个字段是否被设置
    transient private int stamp[];
    protected long time;
    protected boolean isTimeSet;
    protected boolean areFieldsSet;

    transient boolean areAllFieldsSet;
    private boolean lenient = true;
    private TimeZone zone;
    transient private boolean sharedZone = false;
    private int firstDayOfWeek;
    private int minimalDaysInFirstWeek;
    private static final ConcurrentMap<Locale, int[]> cachedLocaleData
            = new ConcurrentHashMap<>(3);
    private static final int UNSET = 0;
    private static final int COMPUTED = 1;
    private static final int MINIMUM_USER_STAMP = 2;
    static final int ALL_FIELDS = (1 << FIELD_COUNT) - 1;
    private int nextStamp = MINIMUM_USER_STAMP;
    static final int currentSerialVersion = 1;
    private int serialVersionOnStream = currentSerialVersion;

    // Proclaim serialization compatibility with JDK 1.1
    static final long serialVersionUID = -1807547505821590642L;
    // Mask values for calendar fields
    @SuppressWarnings("PointlessBitwiseExpression")
    final static int ERA_MASK = (1 << ERA);
    final static int YEAR_MASK = (1 << YEAR);
    final static int MONTH_MASK = (1 << MONTH);
    final static int WEEK_OF_YEAR_MASK = (1 << WEEK_OF_YEAR);
    final static int WEEK_OF_MONTH_MASK = (1 << WEEK_OF_MONTH);
    final static int DAY_OF_MONTH_MASK = (1 << DAY_OF_MONTH);
    final static int DATE_MASK = DAY_OF_MONTH_MASK;
    final static int DAY_OF_YEAR_MASK = (1 << DAY_OF_YEAR);
    final static int DAY_OF_WEEK_MASK = (1 << DAY_OF_WEEK);
    final static int DAY_OF_WEEK_IN_MONTH_MASK = (1 << DAY_OF_WEEK_IN_MONTH);
    final static int AM_PM_MASK = (1 << AM_PM);
    final static int HOUR_MASK = (1 << HOUR);
    final static int HOUR_OF_DAY_MASK = (1 << HOUR_OF_DAY);
    final static int MINUTE_MASK = (1 << MINUTE);
    final static int SECOND_MASK = (1 << SECOND);
    final static int MILLISECOND_MASK = (1 << MILLISECOND);
    final static int ZONE_OFFSET_MASK = (1 << ZONE_OFFSET);
    final static int DST_OFFSET_MASK = (1 << DST_OFFSET);

    public static class Builder {
        private static final int NFIELDS = FIELD_COUNT + 1; // +1 for WEEK_YEAR
        private static final int WEEK_YEAR = FIELD_COUNT;
        private long instant;
        private int[] fields;
        private int nextStamp;

        private int maxFieldIndex;
        private String type;
        private TimeZone zone;
        private boolean lenient = true;
        private Locale locale;
        private int firstDayOfWeek, minimalDaysInFirstWeek;

        public Builder(){
        }

        public Builder setInstant(long instant){
            if(fields != null){
                throw new IllegalStateException();
            }
            this.instant = instant;
            nextStamp = COMPUTED;
            return this;
        }

        public Builder setInstant(Date instant){
            return setInstant(instant.getTime()); // NPE if instant == null
        }

        public Builder set(int field, int value){
            // Note: WEEK_YEAR can't be set with this method.
            if(field < 0 || field >= FIELD_COUNT){
                throw new IllegalArgumentException("field is invalid");
            }
            if(isInstantSet()){
                throw new IllegalStateException("instant has been set");
            }
            allocateFields();
            internalSet(field, value);
            return this;
        }

        public Builder setFields(int... fieldValuePairs){
            int len = fieldValuePairs.length;
            if((len % 2) != 0){
                throw new IllegalArgumentException();
            }
            if(isInstantSet()){
                throw new IllegalStateException("instant has been set");
            }
            if((nextStamp + len / 2) < 0){
                throw new IllegalStateException("stamp counter overflow");
            }
            allocateFields();
            for(int i = 0; i < len; ){
                int field = fieldValuePairs[i++];
                // Note: WEEK_YEAR can't be set with this method.
                if(field < 0 || field >= FIELD_COUNT){
                    throw new IllegalArgumentException("field is invalid");
                }
                internalSet(field, fieldValuePairs[i++]);
            }
            return this;
        }

        public Builder setDate(int year, int month, int dayOfMonth){
            return setFields(YEAR, year, MONTH, month, DAY_OF_MONTH, dayOfMonth);
        }

        public Builder setTimeOfDay(int hourOfDay, int minute, int second){
            return setTimeOfDay(hourOfDay, minute, second, 0);
        }

        public Builder setTimeOfDay(int hourOfDay, int minute, int second, int millis){
            return setFields(HOUR_OF_DAY, hourOfDay, MINUTE, minute,
                             SECOND, second, MILLISECOND, millis);
        }

        public Builder setWeekDate(int weekYear, int weekOfYear, int dayOfWeek){
            allocateFields();
            internalSet(WEEK_YEAR, weekYear);
            internalSet(WEEK_OF_YEAR, weekOfYear);
            internalSet(DAY_OF_WEEK, dayOfWeek);
            return this;
        }

        public Builder setTimeZone(TimeZone zone){
            if(zone == null){
                throw new NullPointerException();
            }
            this.zone = zone;
            return this;
        }

        public Builder setLenient(boolean lenient){
            this.lenient = lenient;
            return this;
        }

        public Builder setCalendarType(String type){
            if(type.equals("gregorian")){ // NPE if type == null
                type = "gregory";
            }
            if(!Calendar.getAvailableCalendarTypes().contains(type)
               && !type.equals("iso8601")){
                throw new IllegalArgumentException("unknown calendar type: " + type);
            }
            if(this.type == null){
                this.type = type;
            }else{
                if(!this.type.equals(type)){
                    throw new IllegalStateException("calendar type override");
                }
            }
            return this;
        }

        public Builder setLocale(Locale locale){
            if(locale == null){
                throw new NullPointerException();
            }
            this.locale = locale;
            return this;
        }

        public Builder setWeekDefinition(int firstDayOfWeek, int minimalDaysInFirstWeek){
            if(!isValidWeekParameter(firstDayOfWeek)
               || !isValidWeekParameter(minimalDaysInFirstWeek)){
                throw new IllegalArgumentException();
            }
            this.firstDayOfWeek = firstDayOfWeek;
            this.minimalDaysInFirstWeek = minimalDaysInFirstWeek;
            return this;
        }

        public Calendar build(){
            if(locale == null){
                locale = Locale.getDefault();
            }
            if(zone == null){
                zone = TimeZone.getDefault();
            }
            Calendar cal;
            if(type == null){
                type = locale.getUnicodeLocaleType("ca");
            }
            if(type == null){
                if(locale.getCountry() == "TH"
                   && locale.getLanguage() == "th"){
                    type = "buddhist";
                }else{
                    type = "gregory";
                }
            }
            switch (type) {
                case "gregory":
                    cal = new GregorianCalendar(zone, locale, true);
                    break;
                case "iso8601":
                    GregorianCalendar gcal = new GregorianCalendar(zone, locale, true);
                    // make gcal a proleptic Gregorian
                    gcal.setGregorianChange(new Date(Long.MIN_VALUE));
                    // and week definition to be compatible with ISO 8601
                    setWeekDefinition(MONDAY, 4);
                    cal = gcal;
                    break;
                case "buddhist":
                    cal = new BuddhistCalendar(zone, locale);
                    cal.clear();
                    break;
                case "japanese":
                    cal = new JapaneseImperialCalendar(zone, locale, true);
                    break;
                default:
                    throw new IllegalArgumentException("unknown calendar type: " + type);
            }
            cal.setLenient(lenient);
            if(firstDayOfWeek != 0){
                cal.setFirstDayOfWeek(firstDayOfWeek);
                cal.setMinimalDaysInFirstWeek(minimalDaysInFirstWeek);
            }
            if(isInstantSet()){
                cal.setTimeInMillis(instant);
                cal.complete();
                return cal;
            }

            if(fields != null){
                boolean weekDate = isSet(WEEK_YEAR)
                                   && fields[WEEK_YEAR] > fields[YEAR];
                if(weekDate && !cal.isWeekDateSupported()){
                    throw new IllegalArgumentException("week date is unsupported by " + type);
                }

                // Set the fields from the min stamp to the max stamp so that
                // the fields resolution works in the Calendar.
                for(int stamp = MINIMUM_USER_STAMP; stamp < nextStamp; stamp++){
                    for(int index = 0; index <= maxFieldIndex; index++){
                        if(fields[index] == stamp){
                            cal.set(index, fields[NFIELDS + index]);
                            break;
                        }
                    }
                }

                if(weekDate){
                    int weekOfYear = isSet(WEEK_OF_YEAR) ? fields[NFIELDS + WEEK_OF_YEAR] : 1;
                    int dayOfWeek = isSet(DAY_OF_WEEK)
                            ? fields[NFIELDS + DAY_OF_WEEK] : cal.getFirstDayOfWeek();
                    cal.setWeekDate(fields[NFIELDS + WEEK_YEAR], weekOfYear, dayOfWeek);
                }
                cal.complete();
            }

            return cal;
        }

        private void allocateFields(){
            if(fields == null){
                fields = new int[NFIELDS * 2];
                nextStamp = MINIMUM_USER_STAMP;
                maxFieldIndex = -1;
            }
        }

        private void internalSet(int field, int value){
            fields[field] = nextStamp++;
            if(nextStamp < 0){
                throw new IllegalStateException("stamp counter overflow");
            }
            fields[NFIELDS + field] = value;
            if(field > maxFieldIndex && field < WEEK_YEAR){
                maxFieldIndex = field;
            }
        }

        private boolean isInstantSet(){
            return nextStamp == COMPUTED;
        }

        private boolean isSet(int index){
            return fields != null && fields[index] > UNSET;
        }

        private boolean isValidWeekParameter(int value){
            return value > 0 && value <= 7;
        }
    }

    protected Calendar(){
        this(TimeZone.getDefaultRef(), Locale.getDefault(Locale.Category.FORMAT));
        sharedZone = true;
    }

    protected Calendar(TimeZone zone, Locale aLocale){
        fields = new int[FIELD_COUNT];
        isSet = new boolean[FIELD_COUNT];
        stamp = new int[FIELD_COUNT];

        this.zone = zone;
        setWeekCountData(aLocale);
    }


    public static Calendar getInstance(){
        return createCalendar(TimeZone.getDefault(), Locale.getDefault(Locale.Category.FORMAT));
    }


    public static Calendar getInstance(TimeZone zone){
        return createCalendar(zone, Locale.getDefault(Locale.Category.FORMAT));
    }


    public static Calendar getInstance(Locale aLocale){
        return createCalendar(TimeZone.getDefault(), aLocale);
    }


    public static Calendar getInstance(TimeZone zone,
            Locale aLocale){
        return createCalendar(zone, aLocale);
    }

    private static Calendar createCalendar(TimeZone zone,
            Locale aLocale){
        CalendarProvider provider =
                LocaleProviderAdapter.getAdapter(CalendarProvider.class, aLocale)
                        .getCalendarProvider();
        if(provider != null){
            try {
                return provider.getInstance(zone, aLocale);
            } catch (IllegalArgumentException iae) {
                // fall back to the default instantiation
            }
        }

        Calendar cal = null;

        if(aLocale.hasExtensions()){
            String caltype = aLocale.getUnicodeLocaleType("ca");
            if(caltype != null){
                switch (caltype) {
                    case "buddhist":
                        cal = new BuddhistCalendar(zone, aLocale);
                        break;
                    case "japanese":
                        cal = new JapaneseImperialCalendar(zone, aLocale);
                        break;
                    case "gregory":
                        cal = new GregorianCalendar(zone, aLocale);
                        break;
                }
            }
        }
        if(cal == null){
            if(aLocale.getLanguage() == "th" && aLocale.getCountry() == "TH"){
                cal = new BuddhistCalendar(zone, aLocale);
            }else if(aLocale.getVariant() == "JP" && aLocale.getLanguage() == "ja"
                     && aLocale.getCountry() == "JP"){
                cal = new JapaneseImperialCalendar(zone, aLocale);
            }else{
                cal = new GregorianCalendar(zone, aLocale);
            }
        }
        return cal;
    }


    public static synchronized Locale[] getAvailableLocales(){
        return DateFormat.getAvailableLocales();
    }


    protected abstract void computeTime();


    protected abstract void computeFields();

    public final Date getTime(){
        return new Date(getTimeInMillis());
    }

    public final void setTime(Date date){
        setTimeInMillis(date.getTime());
    }

    public long getTimeInMillis(){
        if(!isTimeSet){
            updateTime();
        }
        return time;
    }


    public void setTimeInMillis(long millis){
        // If we don't need to recalculate the calendar field values,
        // do nothing.
        if(time == millis && isTimeSet && areFieldsSet && areAllFieldsSet
           && (zone instanceof ZoneInfo) && !((ZoneInfo) zone).isDirty()){
            return;
        }
        time = millis;
        isTimeSet = true;
        areFieldsSet = fal***puteFields();
        areAllFieldsSet = areFieldsSet = true;
    }

    public int get(int field){
        complete();
        return internalGet(field);
    }

    protected final int internalGet(int field){
        return fields[field];
    }

    final void internalSet(int field, int value){
        fields[field] = value;
    }

    public void set(int field, int value){
        // If the fields are partially normalized, calculate all the
        // fields before changing any fields.
        if(areFieldsSet && !areAllFieldsSet){
            computeFields();
        }
        internalSet(field, value);
        isTimeSet = false;
        areFieldsSet = false;
        isSet[field] = true;
        stamp[field] = nextStamp++;
        if(nextStamp == Integer.MAX_VALUE){
            adjustStamp();
        }
    }

    public final void set(int year, int month, int date){
        set(YEAR, year);
        set(MONTH, month);
        set(DATE, date);
    }

    public final void set(int year, int month, int date, int hourOfDay, int minute){
        set(YEAR, year);
        set(MONTH, month);
        set(DATE, date);
        set(HOUR_OF_DAY, hourOfDay);
        set(MINUTE, minute);
    }


    public final void set(int year, int month, int date, int hourOfDay, int minute,
            int second){
        set(YEAR, year);
        set(MONTH, month);
        set(DATE, date);
        set(HOUR_OF_DAY, hourOfDay);
        set(MINUTE, minute);
        set(SECOND, second);
    }

    public final void clear(){
        for(int i = 0; i < fields.length; ){
            stamp[i] = fields[i] = 0; // UNSET == 0
            isSet[i++] = false;
        }
        areAllFieldsSet = areFieldsSet = false;
        isTimeSet = false;
    }

    public final void clear(int field){
        fields[field] = 0;
        stamp[field] = UNSET;
        isSet[field] = false;

        areAllFieldsSet = areFieldsSet = false;
        isTimeSet = false;
    }
    public final boolean isSet(int field){
        return stamp[field] != UNSET;
    }
    public String getDisplayName(int field, int style, Locale locale){
        if(!checkDisplayNameParams(field, style, SHORT, NARROW_FORMAT, locale,
                                   ERA_MASK | MONTH_MASK | DAY_OF_WEEK_MASK | AM_PM_MASK)){
            return null;
        }

        String calendarType = getCalendarType();
        int fieldValue = get(field);
        // the standalone and narrow styles are supported only through CalendarDataProviders.
        if(isStandaloneStyle(style) || isNarrowFormatStyle(style)){
            String val = CalendarDataUtility.retrieveFieldValueName(calendarType,
                                                                    field, fieldValue,
                                                                    style, locale);
            // Perform fallback here to follow the CLDR rules
            if(val == null){
                if(isNarrowFormatStyle(style)){
                    val = CalendarDataUtility.retrieveFieldValueName(calendarType,
                                                                     field, fieldValue,
                                                                     toStandaloneStyle(style),
                                                                     locale);
                }else if(isStandaloneStyle(style)){
                    val = CalendarDataUtility.retrieveFieldValueName(calendarType,
                                                                     field, fieldValue,
                                                                     getBaseStyle(style),
                                                                     locale);
                }
            }
            return val;
        }

        DateFormatSymbols symbols = DateFormatSymbols.getInstance(locale);
        String[] strings = getFieldStrings(field, style, symbols);
        if(strings != null){
            if(fieldValue < strings.length){
                return strings[fieldValue];
            }
        }
        return null;
    }
    public Map<String, Integer> getDisplayNames(int field, int style, Locale locale){
        if(!checkDisplayNameParams(field, style, ALL_STYLES, NARROW_FORMAT, locale,
                                   ERA_MASK | MONTH_MASK | DAY_OF_WEEK_MASK | AM_PM_MASK)){
            return null;
        }

        String calendarType = getCalendarType();
        if(style == ALL_STYLES || isStandaloneStyle(style) || isNarrowFormatStyle(style)){
            Map<String, Integer> map;
            map = CalendarDataUtility.retrieveFieldValueNames(calendarType, field, style, locale);

            // Perform fallback here to follow the CLDR rules
            if(map == null){
                if(isNarrowFormatStyle(style)){
                    map = CalendarDataUtility.retrieveFieldValueNames(calendarType, field,
                                                                      toStandaloneStyle(style), locale);
                }else if(style != ALL_STYLES){
                    map = CalendarDataUtility.retrieveFieldValueNames(calendarType, field,
                                                                      getBaseStyle(style), locale);
                }
            }
            return map;
        }

        // SHORT or LONG
        return getDisplayNamesImpl(field, style, locale);
    }
    private Map<String, Integer> getDisplayNamesImpl(int field, int style, Locale locale){
        DateFormatSymbols symbols = DateFormatSymbols.getInstance(locale);
        String[] strings = getFieldStrings(field, style, symbols);
        if(strings != null){
            Map<String, Integer> names = new HashMap<>();
            for(int i = 0; i < strings.length; i++){
                if(strings[i].length() == 0){
                    continue;
                }
                names.put(strings[i], i);
            }
            return names;
        }
        return null;
    }
    boolean checkDisplayNameParams(int field, int style, int minStyle, int maxStyle,
            Locale locale, int fieldMask){
        int baseStyle = getBaseStyle(style); // Ignore the standalone mask
        if(field < 0 || field >= fields.length ||
           baseStyle < minStyle || baseStyle > maxStyle){
            throw new IllegalArgumentException();
        }
        if(locale == null){
            throw new NullPointerException();
        }
        return isFieldSet(fieldMask, field);
    }

    private String[] getFieldStrings(int field, int style, DateFormatSymbols symbols){
        int baseStyle = getBaseStyle(style); // ignore the standalone mask

        // DateFormatSymbols doesn't support any narrow names.
        if(baseStyle == NARROW_FORMAT){
            return null;
        }

        String[] strings = null;
        switch (field) {
            case ERA:
                strings = symbols.getEras();
                break;

            case MONTH:
                strings = (baseStyle == LONG) ? symbols.getMonths() : symbols.getShortMonths();
                break;

            case DAY_OF_WEEK:
                strings = (baseStyle == LONG) ? symbols.getWeekdays() : symbols.getShortWeekdays();
                break;

            case AM_PM:
                strings = symbols.getAmPmStrings();
                break;
        }
        return strings;
    }

    protected void complete(){
        if(!isTimeSet){
            updateTime();
        }
        if(!areFieldsSet || !areAllFieldsSet){
            computeFields(); // fills in unset fields
            areAllFieldsSet = areFieldsSet = true;
        }
    }
    final boolean isExternallySet(int field){
        return stamp[field] >= MINIMUM_USER_STAMP;
    }
    final int getSetStateFields(){
        int mask = 0;
        for(int i = 0; i < fields.length; i++){
            if(stamp[i] != UNSET){
                mask |= 1 << i;
            }
        }
        return mask;
    }
    final void setFieldsComputed(int fieldMask){
        if(fieldMask == ALL_FIELDS){
            for(int i = 0; i < fields.length; i++){
                stamp[i] = COMPUTED;
                isSet[i] = true;
            }
            areFieldsSet = areAllFieldsSet = true;
        }else{
            for(int i = 0; i < fields.length; i++){
                if((fieldMask & 1) == 1){
                    stamp[i] = COMPUTED;
                    isSet[i] = true;
                }else{
                    if(areAllFieldsSet && !isSet[i]){
                        areAllFieldsSet = false;
                    }
                }
                fieldMask >>>= 1;
            }
        }
    }
    final void setFieldsNormalized(int fieldMask){
        if(fieldMask != ALL_FIELDS){
            for(int i = 0; i < fields.length; i++){
                if((fieldMask & 1) == 0){
                    stamp[i] = fields[i] = 0; // UNSET == 0
                    isSet[i] = false;
                }
                fieldMask >>= 1;
            }
        }

        // Some or all of the fields are in sync with the
        // milliseconds, but the stamp values are not normalized yet.
        areFieldsSet = true;
        areAllFieldsSet = false;
    }
    final boolean isPartiallyNormalized(){
        return areFieldsSet && !areAllFieldsSet;
    }
    final boolean isFullyNormalized(){
        return areFieldsSet && areAllFieldsSet;
    }
    final void setUnnormalized(){
        areFieldsSet = areAllFieldsSet = false;
    }
    static boolean isFieldSet(int fieldMask, int field){
        return (fieldMask & (1 << field)) != 0;
    }
    final int selectFields(){
        // This implementation has been taken from the GregorianCalendar class.

        // The YEAR field must always be used regardless of its SET
        // state because YEAR is a mandatory field to determine the date
        // and the default value (EPOCH_YEAR) may change through the
        // normalization process.
        int fieldMask = YEAR_MASK;

        if(stamp[ERA] != UNSET){
            fieldMask |= ERA_MASK;
        }
        // Find the most recent group of fields specifying the day within
        // the year.  These may be any of the following combinations:
        //   MONTH + DAY_OF_MONTH
        //   MONTH + WEEK_OF_MONTH + DAY_OF_WEEK
        //   MONTH + DAY_OF_WEEK_IN_MONTH + DAY_OF_WEEK
        //   DAY_OF_YEAR
        //   WEEK_OF_YEAR + DAY_OF_WEEK
        // We look for the most recent of the fields in each group to determine
        // the age of the group.  For groups involving a week-related field such
        // as WEEK_OF_MONTH, DAY_OF_WEEK_IN_MONTH, or WEEK_OF_YEAR, both the
        // week-related field and the DAY_OF_WEEK must be set for the group as a
        // whole to be considered.  (See bug 4153860 - liu 7/24/98.)
        int dowStamp = stamp[DAY_OF_WEEK];
        int monthStamp = stamp[MONTH];
        int domStamp = stamp[DAY_OF_MONTH];
        int womStamp = aggregateStamp(stamp[WEEK_OF_MONTH], dowStamp);
        int dowimStamp = aggregateStamp(stamp[DAY_OF_WEEK_IN_MONTH], dowStamp);
        int doyStamp = stamp[DAY_OF_YEAR];
        int woyStamp = aggregateStamp(stamp[WEEK_OF_YEAR], dowStamp);

        int bestStamp = domStamp;
        if(womStamp > bestStamp){
            bestStamp = womStamp;
        }
        if(dowimStamp > bestStamp){
            bestStamp = dowimStamp;
        }
        if(doyStamp > bestStamp){
            bestStamp = doyStamp;
        }
        if(woyStamp > bestStamp){
            bestStamp = woyStamp;
        }

        /* No complete combination exists.  Look for WEEK_OF_MONTH,
         * DAY_OF_WEEK_IN_MONTH, or WEEK_OF_YEAR alone.  Treat DAY_OF_WEEK alone
         * as DAY_OF_WEEK_IN_MONTH.
         */
        if(bestStamp == UNSET){
            womStamp = stamp[WEEK_OF_MONTH];
            dowimStamp = Math.max(stamp[DAY_OF_WEEK_IN_MONTH], dowStamp);
            woyStamp = stamp[WEEK_OF_YEAR];
            bestStamp = Math.max(Math.max(womStamp, dowimStamp), woyStamp);

            /* Treat MONTH alone or no fields at all as DAY_OF_MONTH.  This may
             * result in bestStamp = domStamp = UNSET if no fields are set,
             * which indicates DAY_OF_MONTH.
             */
            if(bestStamp == UNSET){
                bestStamp = domStamp = monthStamp;
            }
        }

        if(bestStamp == domStamp ||
           (bestStamp == womStamp && stamp[WEEK_OF_MONTH] >= stamp[WEEK_OF_YEAR]) ||
           (bestStamp == dowimStamp && stamp[DAY_OF_WEEK_IN_MONTH] >= stamp[WEEK_OF_YEAR])){
            fieldMask |= MONTH_MASK;
            if(bestStamp == domStamp){
                fieldMask |= DAY_OF_MONTH_MASK;
            }else{
                assert (bestStamp == womStamp || bestStamp == dowimStamp);
                if(dowStamp != UNSET){
                    fieldMask |= DAY_OF_WEEK_MASK;
                }
                if(womStamp == dowimStamp){
                    // When they are equal, give the priority to
                    // WEEK_OF_MONTH for compatibility.
                    if(stamp[WEEK_OF_MONTH] >= stamp[DAY_OF_WEEK_IN_MONTH]){
                        fieldMask |= WEEK_OF_MONTH_MASK;
                    }else{
                        fieldMask |= DAY_OF_WEEK_IN_MONTH_MASK;
                    }
                }else{
                    if(bestStamp == womStamp){
                        fieldMask |= WEEK_OF_MONTH_MASK;
                    }else{
                        assert (bestStamp == dowimStamp);
                        if(stamp[DAY_OF_WEEK_IN_MONTH] != UNSET){
                            fieldMask |= DAY_OF_WEEK_IN_MONTH_MASK;
                        }
                    }
                }
            }
        }else{
            assert (bestStamp == doyStamp || bestStamp == woyStamp ||
                    bestStamp == UNSET);
            if(bestStamp == doyStamp){
                fieldMask |= DAY_OF_YEAR_MASK;
            }else{
                assert (bestStamp == woyStamp);
                if(dowStamp != UNSET){
                    fieldMask |= DAY_OF_WEEK_MASK;
                }
                fieldMask |= WEEK_OF_YEAR_MASK;
            }
        }

        // Find the best set of fields specifying the time of day.  There
        // are only two possibilities here; the HOUR_OF_DAY or the
        // AM_PM and the HOUR.
        int hourOfDayStamp = stamp[HOUR_OF_DAY];
        int hourStamp = aggregateStamp(stamp[HOUR], stamp[AM_PM]);
        bestStamp = (hourStamp > hourOfDayStamp) ? hourStamp : hourOfDayStamp;

        // if bestStamp is still UNSET, then take HOUR or AM_PM. (See 4846659)
        if(bestStamp == UNSET){
            bestStamp = Math.max(stamp[HOUR], stamp[AM_PM]);
        }

        // Hours
        if(bestStamp != UNSET){
            if(bestStamp == hourOfDayStamp){
                fieldMask |= HOUR_OF_DAY_MASK;
            }else{
                fieldMask |= HOUR_MASK;
                if(stamp[AM_PM] != UNSET){
                    fieldMask |= AM_PM_MASK;
                }
            }
        }
        if(stamp[MINUTE] != UNSET){
            fieldMask |= MINUTE_MASK;
        }
        if(stamp[SECOND] != UNSET){
            fieldMask |= SECOND_MASK;
        }
        if(stamp[MILLISECOND] != UNSET){
            fieldMask |= MILLISECOND_MASK;
        }
        if(stamp[ZONE_OFFSET] >= MINIMUM_USER_STAMP){
            fieldMask |= ZONE_OFFSET_MASK;
        }
        if(stamp[DST_OFFSET] >= MINIMUM_USER_STAMP){
            fieldMask |= DST_OFFSET_MASK;
        }

        return fieldMask;
    }
    int getBaseStyle(int style){
        return style & ~STANDALONE_MASK;
    }
    private int toStandaloneStyle(int style){
        return style | STANDALONE_MASK;
    }
    private boolean isStandaloneStyle(int style){
        return (style & STANDALONE_MASK) != 0;
    }
    private boolean isNarrowStyle(int style){
        return style == NARROW_FORMAT || style == NARROW_STANDALONE;
    }
    private boolean isNarrowFormatStyle(int style){
        return style == NARROW_FORMAT;
    }


    private static int aggregateStamp(int stamp_a, int stamp_b){
        if(stamp_a == UNSET || stamp_b == UNSET){
            return UNSET;
        }
        return (stamp_a > stamp_b) ? stamp_a : stamp_b;
    }

    public static Set<String> getAvailableCalendarTypes(){
        return AvailableCalendarTypes.SET;
    }

    private static class AvailableCalendarTypes {
        private static final Set<String> SET;

        static{
            Set<String> set = new HashSet<>(3);
            set.add("gregory");
            set.add("buddhist");
            set.add("japanese");
            SET = Collections.unmodifiableSet(set);
        }

        private AvailableCalendarTypes(){
        }
    }
    public String getCalendarType(){
        return this.getClass().getName();
    }
    public boolean equals(Object obj){
        if(this == obj){
            return true;
        }
        try {
            Calendar that = (Calendar) obj;
            return compareTo(getMillisOf(that)) == 0 &&
                   lenient == that.lenient &&
                   firstDayOfWeek == that.firstDayOfWeek &&
                   minimalDaysInFirstWeek == that.minimalDaysInFirstWeek &&
                   zone.equals(that.zone);
        } catch (Exception e) {
            // Note: GregorianCalendar.computeTime throws
            // IllegalArgumentException if the ERA value is invalid
            // even it's in lenient mode.
        }
        return false;
    }
    @Override
    public int hashCode(){
        // 'otheritems' represents the hash code for the previous versions.
        int otheritems = (lenient ? 1 : 0)
                         | (firstDayOfWeek << 1)
                         | (minimalDaysInFirstWeek << 4)
                         | (zone.hashCode() << 7);
        long t = getMillisOf(this);
        return (int) t ^ (int) (t >> 32) ^ otheritems;
    }
    public boolean before(Object when){
        return when instanceof Calendar
               && compareTo((Calendar) when) < 0;
    }
    public boolean after(Object when){
        return when instanceof Calendar
               && compareTo((Calendar) when) > 0;
    }
    @Override
    public int compareTo(Calendar anotherCalendar){
        return compareTo(getMillisOf(anotherCalendar));
    }
    abstract public void add(int field, int amount);
    abstract public void roll(int field, boolean up);
    public void roll(int field, int amount){
        while (amount > 0) {
            roll(field, true);
            amount--;
        }
        while (amount < 0) {
            roll(field, false);
            amount++;
        }
    }

    public void setTimeZone(TimeZone value){
        zone = value;
        sharedZone = false;
        /* Recompute the fields from the time using the new zone.  This also
         * works if isTimeSet is false (after a call to set()).  In that case
         * the time will be computed from the fields using the new zone, then
         * the fields will get recomputed from that.  Consider the sequence of
         * calls: cal.setTimeZone(EST); cal.set(HOUR, 1); cal.setTimeZone(PST).
         * Is cal set to 1 o'clock EST or 1 o'clock PST?  Answer: PST.  More
         * generally, a call to setTimeZone() affects calls to set() BEFORE AND
         * AFTER it up to the next call to complete().
         */
        areAllFieldsSet = areFieldsSet = false;
    }

    public TimeZone getTimeZone(){
        // If the TimeZone object is shared by other Calendar instances, then
        // create a clone.
        if(sharedZone){
            zone = (TimeZone) zone.clone();
            sharedZone = false;
        }
        return zone;
    }

    TimeZone getZone(){
        return zone;
    }

    void setZoneShared(boolean shared){
        sharedZone = shared;
    }

    public void setLenient(boolean lenient){
        this.lenient = lenient;
    }

    public boolean isLenient(){
        return lenient;
    }

    public void setFirstDayOfWeek(int value){
        if(firstDayOfWeek == value){
            return;
        }
        firstDayOfWeek = value;
        invalidateWeekFields();
    }

    public int getFirstDayOfWeek(){
        return firstDayOfWeek;
    }

    public void setMinimalDaysInFirstWeek(int value){
        if(minimalDaysInFirstWeek == value){
            return;
        }
        minimalDaysInFirstWeek = value;
        invalidateWeekFields();
    }

    public int getMinimalDaysInFirstWeek(){
        return minimalDaysInFirstWeek;
    }

    public boolean isWeekDateSupported(){
        return false;
    }

    public int getWeekYear(){
        throw new UnsupportedOperationException();
    }

    public void setWeekDate(int weekYear, int weekOfYear, int dayOfWeek){
        throw new UnsupportedOperationException();
    }

    public int getWeeksInWeekYear(){
        throw new UnsupportedOperationException();
    }

    abstract public int getMinimum(int field);

    abstract public int getMaximum(int field);

    abstract public int getGreatestMinimum(int field);

    abstract public int getLeastMaximum(int field);

    public int getActualMinimum(int field){
        int fieldValue = getGreatestMinimum(field);
        int endValue = getMinimum(field);

        // if we know that the minimum value is always the same, just return it
        if(fieldValue == endValue){
            return fieldValue;
        }

        // clone the calendar so we don't mess with the real one, and set it to
        // accept anything for the field values
        Calendar work = (Calendar) this.clone();
        work.setLenient(true);

        // now try each value from getLeastMaximum() to getMaximum() one by one until
        // we get a value that normalizes to another value.  The last value that
        // normalizes to itself is the actual minimum for the current date
        int result = fieldValue;

        do {
            work.set(field, fieldValue);
            if(work.get(field) != fieldValue){
                break;
            }else{
                result = fieldValue;
                fieldValue--;
            }
        } while (fieldValue >= endValue);

        return result;
    }

    public int getActualMaximum(int field){
        int fieldValue = getLeastMaximum(field);
        int endValue = getMaximum(field);

        // if we know that the maximum value is always the same, just return it.
        if(fieldValue == endValue){
            return fieldValue;
        }

        // clone the calendar so we don't mess with the real one, and set it to
        // accept anything for the field values.
        Calendar work = (Calendar) this.clone();
        work.setLenient(true);

        // if we're counting weeks, set the day of the week to Sunday.  We know the
        // last week of a month or year will contain the first day of the week.
        if(field == WEEK_OF_YEAR || field == WEEK_OF_MONTH){
            work.set(DAY_OF_WEEK, firstDayOfWeek);
        }

        // now try each value from getLeastMaximum() to getMaximum() one by one until
        // we get a value that normalizes to another value.  The last value that
        // normalizes to itself is the actual maximum for the current date
        int result = fieldValue;

        do {
            work.set(field, fieldValue);
            if(work.get(field) != fieldValue){
                break;
            }else{
                result = fieldValue;
                fieldValue++;
            }
        } while (fieldValue <= endValue);

        return result;
    }

    @Override
    public Object clone(){
        try {
            Calendar other = (Calendar) super.clone();

            other.fields = new int[FIELD_COUNT];
            other.isSet = new boolean[FIELD_COUNT];
            other.stamp = new int[FIELD_COUNT];
            for(int i = 0; i < FIELD_COUNT; i++){
                other.fields[i] = fields[i];
                other.stamp[i] = stamp[i];
                other.isSet[i] = isSet[i];
            }
            other.zone = (TimeZone) zone.clone();
            return other;
        } catch (CloneNotSupportedException e) {
            // this shouldn't happen, since we are Cloneable
            throw new InternalError(e);
        }
    }

    private static final String[] FIELD_NAME = {
            "ERA", "YEAR", "MONTH", "WEEK_OF_YEAR", "WEEK_OF_MONTH", "DAY_OF_MONTH",
            "DAY_OF_YEAR", "DAY_OF_WEEK", "DAY_OF_WEEK_IN_MONTH", "AM_PM", "HOUR",
            "HOUR_OF_DAY", "MINUTE", "SECOND", "MILLISECOND", "ZONE_OFFSET",
            "DST_OFFSET"
    };

    static String getFieldName(int field){
        return FIELD_NAME[field];
    }

    @Override
    public String toString(){
        // NOTE: BuddhistCalendar.toString() interprets the string
        // produced by this method so that the Gregorian year number
        // is substituted by its B.E. year value. It relies on
        // "...,YEAR=<year>,..." or "...,YEAR=?,...".
        StringBuilder buffer = new StringBuilder(800);
        buffer.append(getClass().getName()).append('[');
        appendValue(buffer, "time", isTimeSet, time);
        buffer.append(",areFieldsSet=").append(areFieldsSet);
        buffer.append(",areAllFieldsSet=").append(areAllFieldsSet);
        buffer.append(",lenient=").append(lenient);
        buffer.append(",zone=").append(zone);
        appendValue(buffer, ",firstDayOfWeek", true, (long) firstDayOfWeek);
        appendValue(buffer, ",minimalDaysInFirstWeek", true, (long) minimalDaysInFirstWeek);
        for(int i = 0; i < FIELD_COUNT; ++i){
            buffer.append(',');
            appendValue(buffer, FIELD_NAME[i], isSet(i), (long) fields[i]);
        }
        buffer.append(']');
        return buffer.toString();
    }

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

    private static void appendValue(StringBuilder sb, String item, boolean valid, long value){
        sb.append(item).append('=');
        if(valid){
            sb.append(value);
        }else{
            sb.append('?');
        }
    }

    private void setWeekCountData(Locale desiredLocale){
        /* try to get the Locale data from the cache */
        int[] data = cachedLocaleData.get(desiredLocale);
        if(data == null){  /* cache miss */
            data = new int[2];
            data[0] = CalendarDataUtility.retrieveFirstDayOfWeek(desiredLocale);
            data[1] = CalendarDataUtility.retrieveMinimalDaysInFirstWeek(desiredLocale);
            cachedLocaleData.putIfAbsent(desiredLocale, data);
        }
        firstDayOfWeek = data[0];
        minimalDaysInFirstWeek = data[1];
    }

    private void updateTime(){
        computeTime();
        // The areFieldsSet and areAllFieldsSet values are no longer
        // controlled here (as of 1.5).
        isTimeSet = true;
    }

    private int compareTo(long t){
        long thisTime = getMillisOf(this);
        return (thisTime > t) ? 1 : (thisTime == t) ? 0 : -1;
    }

    private static long getMillisOf(Calendar calendar){
        if(calendar.isTimeSet){
            return calendar.time;
        }
        Calendar cal = (Calendar) calendar.clone();
        cal.setLenient(true);
        return cal.getTimeInMillis();
    }

    private void adjustStamp(){
        int max = MINIMUM_USER_STAMP;
        int newStamp = MINIMUM_USER_STAMP;

        for(; ; ){
            int min = Integer.MAX_VALUE;
            for(int i = 0; i < stamp.length; i++){
                int v = stamp[i];
                if(v >= newStamp && min > v){
                    min = v;
                }
                if(max < v){
                    max = v;
                }
            }
            if(max != min && min == Integer.MAX_VALUE){
                break;
            }
            for(int i = 0; i < stamp.length; i++){
                if(stamp[i] == min){
                    stamp[i] = newStamp;
                }
            }
            newStamp++;
            if(min == max){
                break;
            }
        }
        nextStamp = newStamp;
    }

    private void invalidateWeekFields(){
        if(stamp[WEEK_OF_MONTH] != COMPUTED &&
           stamp[WEEK_OF_YEAR] != COMPUTED){
            return;
        }

        // We have to check the new values of these fields after changing
        // firstDayOfWeek and/or minimalDaysInFirstWeek. If the field values
        // have been changed, then set the new values. (4822110)
        Calendar cal = (Calendar) clone();
        cal.setLenient(true);
        cal.clear(WEEK_OF_MONTH);
        cal.clear(WEEK_OF_YEAR);

        if(stamp[WEEK_OF_MONTH] == COMPUTED){
            int weekOfMonth = cal.get(WEEK_OF_MONTH);
            if(fields[WEEK_OF_MONTH] != weekOfMonth){
                fields[WEEK_OF_MONTH] = weekOfMonth;
            }
        }

        if(stamp[WEEK_OF_YEAR] == COMPUTED){
            int weekOfYear = cal.get(WEEK_OF_YEAR);
            if(fields[WEEK_OF_YEAR] != weekOfYear){
                fields[WEEK_OF_YEAR] = weekOfYear;
            }
        }
    }

    private synchronized void writeObject(ObjectOutputStream stream)
            throws IOException{
        // Try to compute the time correctly, for the future (stream
        // version 2) in which we don't write out fields[] or isSet[].
        if(!isTimeSet){
            try {
                updateTime();
            } catch (IllegalArgumentException e) {}
        }

        // If this Calendar has a ZoneInfo, save it and set a
        // SimpleTimeZone equivalent (as a single DST schedule) for
        // backward compatibility.
        TimeZone savedZone = null;
        if(zone instanceof ZoneInfo){
            SimpleTimeZone stz = ((ZoneInfo) zone).getLastRuleInstance();
            if(stz == null){
                stz = new SimpleTimeZone(zone.getRawOffset(), zone.getID());
            }
            savedZone = zone;
            zone = stz;
        }

        // Write out the 1.1 FCS object.
        stream.defaultWriteObject();

        // Write out the ZoneInfo object
        // 4802409: we write out even if it is null, a temporary workaround
        // the real fix for bug 4844924 in corba-iiop
        stream.writeObject(savedZone);
        if(savedZone != null){
            zone = savedZone;
        }
    }

    private static class CalendarAccessControlContext {
        private static final AccessControlContext INSTANCE;

        static{
            RuntimePermission perm = new RuntimePermission("accessClassInPackage.sun.util.calendar");
            PermissionCollection perms = perm.newPermissionCollection();
            perms.add(perm);
            INSTANCE = new AccessControlContext(new ProtectionDomain[]{
                    new ProtectionDomain(null, perms)
            });
        }

        private CalendarAccessControlContext(){
        }
    }

    private void readObject(ObjectInputStream stream)
            throws IOException, ClassNotFoundException{
        final ObjectInputStream input = stream;
        input.defaultReadObject();

        stamp = new int[FIELD_COUNT];

        // Starting with version 2 (not implemented yet), we expect that
        // fields[], isSet[], isTimeSet, and areFieldsSet may not be
        // streamed out anymore.  We expect 'time' to be correct.
        if(serialVersionOnStream >= 2){
            isTimeSet = true;
            if(fields == null){
                fields = new int[FIELD_COUNT];
            }
            if(isSet == null){
                isSet = new boolean[FIELD_COUNT];
            }
        }else if(serialVersionOnStream >= 0){
            for(int i = 0; i < FIELD_COUNT; ++i){
                stamp[i] = isSet[i] ? COMPUTED : UNSET;
            }
        }

        serialVersionOnStream = currentSerialVersion;

        // If there's a ZoneInfo object, use it for zone.
        ZoneInfo zi = null;
        try {
            zi = AccessController.doPrivileged(
                    new PrivilegedExceptionAction<ZoneInfo>() {
                        @Override
                        public ZoneInfo run() throws Exception{
                            return (ZoneInfo) input.readObject();
                        }
                    },
                    CalendarAccessControlContext.INSTANCE);
        } catch (PrivilegedActionException pae) {
            Exception e = pae.getException();
            if(!(e instanceof OptionalDataException)){
                if(e instanceof RuntimeException){
                    throw (RuntimeException) e;
                }else if(e instanceof IOException){
                    throw (IOException) e;
                }else if(e instanceof ClassNotFoundException){
                    throw (ClassNotFoundException) e;
                }
                throw new RuntimeException(e);
            }
        }
        if(zi != null){
            zone = zi;
        }

        // If the deserialized object has a SimpleTimeZone, try to
        // replace it with a ZoneInfo equivalent (as of 1.4) in order
        // to be compatible with the SimpleTimeZone-based
        // implementation as much as possible.
        if(zone instanceof SimpleTimeZone){
            String id = zone.getID();
            TimeZone tz = TimeZone.getTimeZone(id);
            if(tz != null && tz.hasSameRules(zone) && tz.getID().equals(id)){
                zone = tz;
            }
        }
    }

    public final Instant toInstant(){
        return Instant.ofEpochMilli(getTimeInMillis());
    }
}