// ==++==
//
//   Copyright (c) Microsoft Corporation.  All rights reserved.
//
// ==--==
namespace System.Globalization {
    using System;
    using System.Diagnostics.Contracts;

    ////////////////////////////////////////////////////////////////////////////
    //
    //  Notes about EastAsianLunisolarCalendar
    //
    ////////////////////////////////////////////////////////////////////////////


    [Serializable]
[System.Runtime.InteropServices.ComVisible(true)]
    public abstract class EastAsianLunisolarCalendar : Calendar {
        internal const int LeapMonth = 0;
        internal const int Jan1Month = 1;
        internal const int Jan1Date = 2;
        internal const int nDaysPerMonth = 3;

        // # of days so far in the solar year
        internal static readonly int[] DaysToMonth365 =
        {
            0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334
        };

        internal static readonly int[] DaysToMonth366 =
        {
            0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335
        };

        internal const int DatePartYear = 0;
        internal const int DatePartDayOfYear = 1;
        internal const int DatePartMonth = 2;
        internal const int DatePartDay = 3;

        // Return the type of the East Asian Lunisolar calendars.
        //

        public override CalendarAlgorithmType AlgorithmType {
            get {
                return CalendarAlgorithmType.LunisolarCalendar;
            }
        }

        // Return the year number in the 60-year cycle.
        //

        public virtual int GetSexagenaryYear (DateTime time) {
            CheckTicksRange(time.Ticks);

            int year = 0, month = 0, day = 0;
            TimeToLunar(time, ref year, ref month, ref day);

            return ((year - 4) % 60) + 1;
        }

        // Return the celestial year from the 60-year cycle.
        // The returned value is from 1 ~ 10.
        //

        public int GetCelestialStem(int sexagenaryYear) {
            if ((sexagenaryYear < 1) || (sexagenaryYear > 60)) {
                throw new ArgumentOutOfRangeException(
                                "sexagenaryYear",
                                Environment.GetResourceString("ArgumentOutOfRange_Range", 1, 60));
            }
            Contract.EndContractBlock();

         return ((sexagenaryYear - 1) % 10) + 1;
        }

        // Return the Terrestial Branch from the the 60-year cycle.
        // The returned value is from 1 ~ 12.
        //

        public int GetTerrestrialBranch(int sexagenaryYear) {
            if ((sexagenaryYear < 1) || (sexagenaryYear > 60)) {
                throw new ArgumentOutOfRangeException(
                                "sexagenaryYear",
                                Environment.GetResourceString("ArgumentOutOfRange_Range", 1, 60));
            }
            Contract.EndContractBlock();

            return ((sexagenaryYear - 1) % 12) + 1;
        }

        internal abstract int  GetYearInfo(int LunarYear, int Index);
        internal abstract int GetYear(int year, DateTime time);
        internal abstract int GetGregorianYear(int year, int era);

        internal abstract int MinCalendarYear {get;}
        internal abstract int MaxCalendarYear {get;}
        internal abstract EraInfo[] CalEraInfo{get;}
        internal abstract DateTime MinDate {get;}
        internal abstract DateTime MaxDate {get;}

        internal const int MaxCalendarMonth = 13;
        internal const int MaxCalendarDay = 30;

        internal int MinEraCalendarYear (int era) {
            EraInfo[] mEraInfo = CalEraInfo;
            //ChineseLunisolarCalendar does not has m_EraInfo it is going to retuen null
            if (mEraInfo == null) {
                return MinCalendarYear;
            }

            if (era == Calendar.CurrentEra) {
                era = CurrentEraValue;
            }
            //era has to be in the supported range otherwise we will throw exception in CheckEraRange()
            if (era == GetEra(MinDate)) {
                return (GetYear(MinCalendarYear, MinDate));
            }

            for (int i = 0; i < mEraInfo.Length; i++) {
                if (era == mEraInfo[i].era) {
                    return (mEraInfo[i].minEraYear);
                }
            }
            throw new ArgumentOutOfRangeException("era", Environment.GetResourceString("ArgumentOutOfRange_InvalidEraValue"));
        }

        internal int MaxEraCalendarYear (int era) {
            EraInfo[] mEraInfo = CalEraInfo;
            //ChineseLunisolarCalendar does not has m_EraInfo it is going to retuen null
            if (mEraInfo == null) {
                return MaxCalendarYear;
            }

            if (era == Calendar.CurrentEra) {
                era = CurrentEraValue;
            }
            //era has to be in the supported range otherwise we will throw exception in CheckEraRange()
            if (era == GetEra(MaxDate)) {
                return (GetYear(MaxCalendarYear, MaxDate));
            }

            for (int i = 0; i < mEraInfo.Length; i++) {
                if (era == mEraInfo[i].era) {
                    return (mEraInfo[i].maxEraYear);
                }
            }
            throw new ArgumentOutOfRangeException("era", Environment.GetResourceString("ArgumentOutOfRange_InvalidEraValue"));
        }

        // Construct an instance of EastAsianLunisolar calendar.

        internal EastAsianLunisolarCalendar() {
        }

        internal void CheckTicksRange(long ticks) {
            if (ticks < MinSupportedDateTime.Ticks || ticks > MaxSupportedDateTime.Ticks) {
                throw new ArgumentOutOfRangeException(
                                "time",
                                String.Format(CultureInfo.InvariantCulture, Environment.GetResourceString("ArgumentOutOfRange_CalendarRange"),
                                MinSupportedDateTime, MaxSupportedDateTime));
            }
            Contract.EndContractBlock();
        }

        internal void CheckEraRange (int era) {
            if (era == Calendar.CurrentEra) {
                era = CurrentEraValue;
            }

            if ((era <GetEra(MinDate)) || (era > GetEra(MaxDate))) {
                throw new ArgumentOutOfRangeException("era", Environment.GetResourceString("ArgumentOutOfRange_InvalidEraValue"));
            }
        }

        internal int CheckYearRange(int year, int era) {
         CheckEraRange(era);
         year = GetGregorianYear(year, era);

            if ((year < MinCalendarYear) || (year > MaxCalendarYear)) {
                throw new ArgumentOutOfRangeException(
                                "year",
                                Environment.GetResourceString("ArgumentOutOfRange_Range", MinEraCalendarYear(era), MaxEraCalendarYear(era)));
            }
            return year;
        }

        internal int CheckYearMonthRange(int year, int month, int era) {
            year = CheckYearRange(year, era);

            if (month == 13)
            {
                //Reject if there is no leap month this year
                if (GetYearInfo(year , LeapMonth) == 0)
                    throw new ArgumentOutOfRangeException("month", Environment.GetResourceString("ArgumentOutOfRange_Month"));
            }

            if (month < 1 || month > 13) {
                throw new ArgumentOutOfRangeException("month", Environment.GetResourceString("ArgumentOutOfRange_Month"));
            }
            return year;
        }

        internal int InternalGetDaysInMonth(int year, int month) {
            int nDays;
            int mask;        // mask for extracting bits

            mask = 0x8000;
            // convert the lunar day into a lunar month/date
            mask >>= (month-1);
            if ((GetYearInfo(year, nDaysPerMonth) & mask)== 0)
                nDays = 29;
            else
                nDays = 30;
            return nDays;
        }

        // Returns the number of days in the month given by the year and
        // month arguments.
        //

        public override int GetDaysInMonth(int year, int month, int era) {
            year = CheckYearMonthRange(year, month, era);
            return InternalGetDaysInMonth(year, month);
        }

        static int GregorianIsLeapYear(int y) {
            return ((((y)%4)!=0)?0:((((y)%100)!=0)?1:((((y)%400)!=0)?0:1)));
        }

        // Returns the date and time converted to a DateTime value.  Throws an exception if the n-tuple is invalid.
        //

        public override DateTime ToDateTime(int year, int month, int day, int hour, int minute, int second, int millisecond, int era) {
            year = CheckYearMonthRange(year, month, era);
            int daysInMonth = InternalGetDaysInMonth(year, month);
            if (day < 1 || day > daysInMonth) {
                BCLDebug.Log("year = " + year + ", month = " + month + ", day = " + day);
                throw new ArgumentOutOfRangeException(
                            "day",
                            Environment.GetResourceString("ArgumentOutOfRange_Day", daysInMonth, month));
            }

            int gy=0; int gm=0; int gd=0;

            if (LunarToGregorian(year, month, day, ref gy, ref gm, ref gd)) {
                return new DateTime(gy, gm, gd, hour, minute, second, millisecond);
            } else {
                throw new ArgumentOutOfRangeException(null, Environment.GetResourceString("ArgumentOutOfRange_BadYearMonthDay"));
            }
        }


        //
        // GregorianToLunar calculates lunar calendar info for the given gregorian year, month, date.
        // The input date should be validated before calling this method.
        //
        internal void GregorianToLunar(int nSYear, int nSMonth, int nSDate, ref int nLYear, ref int nLMonth, ref int nLDate) {
            //    unsigned int nLYear, nLMonth, nLDate;    // lunar ymd
            int nSolarDay;        // day # in solar year
            int nLunarDay;        // day # in lunar year
            int fLeap;                    // is it a solar leap year?
            int LDpM;        // lunar days/month bitfield
            int mask;        // mask for extracting bits
            int nDays;        // # days this lunar month
            int nJan1Month, nJan1Date;

            // calc the solar day of year
            fLeap = GregorianIsLeapYear(nSYear);
            nSolarDay = (fLeap==1) ? DaysToMonth366[nSMonth-1]: DaysToMonth365[nSMonth-1] ;
            nSolarDay += nSDate;

            // init lunar year info
            nLunarDay = nSolarDay;
            nLYear = nSYear;
            if (nLYear == (MaxCalendarYear + 1)) {
                nLYear--;
                nLunarDay += ((GregorianIsLeapYear(nLYear) == 1) ? 366 : 365);
                nJan1Month = GetYearInfo(nLYear, Jan1Month);
                nJan1Date = GetYearInfo(nLYear,Jan1Date);
            } else {

                nJan1Month = GetYearInfo(nLYear, Jan1Month);
                nJan1Date = GetYearInfo(nLYear,Jan1Date);

                // check if this solar date is actually part of the previous
                // lunar year
                if ((nSMonth < nJan1Month) ||
                    (nSMonth == nJan1Month && nSDate < nJan1Date)) {
                    // the corresponding lunar day is actually part of the previous
                    // lunar year
                    nLYear--;

                    // add a solar year to the lunar day #
                    nLunarDay += ((GregorianIsLeapYear(nLYear) == 1) ? 366 : 365);

                    // update the new start of year
                    nJan1Month = GetYearInfo(nLYear, Jan1Month);
                    nJan1Date = GetYearInfo(nLYear, Jan1Date);
                }
            }

            // convert solar day into lunar day.
            // subtract off the beginning part of the solar year which is not
            // part of the lunar year.  since this part is always in Jan or Feb,
            // we don't need to handle Leap Year (LY only affects Microsoft
            // and later).
            nLunarDay -= DaysToMonth365[nJan1Month-1];
            nLunarDay -= (nJan1Date - 1);

            // convert the lunar day into a lunar month/date
            mask = 0x8000;
            LDpM = GetYearInfo(nLYear, nDaysPerMonth);
            nDays = ((LDpM & mask) != 0) ? 30 : 29;
            nLMonth = 1;
            while (nLunarDay > nDays) {
                nLunarDay -= nDays;
                nLMonth++;
                mask >>= 1;
                nDays = ((LDpM & mask) != 0) ? 30 : 29;
            }
            nLDate = nLunarDay;
        }

        /*
        //Convert from Lunar to Gregorian
        //Highly inefficient, but it works based on the forward conversion
        */
        internal bool LunarToGregorian(int nLYear, int nLMonth, int nLDate, ref int nSolarYear, ref int nSolarMonth, ref int nSolarDay) {
            int numLunarDays;

            if (nLDate < 1 || nLDate > 30)
                return false;

            numLunarDays = nLDate-1;

            //Add previous months days to form the total num of days from the first of the month.
            for (int i = 1; i < nLMonth; i++) {
                numLunarDays += InternalGetDaysInMonth(nLYear, i);
            }

            //Get Gregorian First of year
            int nJan1Month = GetYearInfo(nLYear, Jan1Month);
            int nJan1Date = GetYearInfo(nLYear, Jan1Date);

            // calc the solar day of year of 1 Lunar day
            int fLeap = GregorianIsLeapYear(nLYear);
            int[] days = (fLeap==1)? DaysToMonth366: DaysToMonth365;

            nSolarDay  = nJan1Date;

            if (nJan1Month > 1)
                nSolarDay += days [nJan1Month-1];

            // Add the actual lunar day to get the solar day we want
            nSolarDay = nSolarDay + numLunarDays;// - 1;

            if ( nSolarDay > (fLeap + 365)) {
                nSolarYear = nLYear + 1;
                nSolarDay -= (fLeap + 365);
            } else {
                nSolarYear = nLYear;
            }

            for (nSolarMonth = 1; nSolarMonth < 12; nSolarMonth++) {
                if (days[nSolarMonth] >= nSolarDay)
                    break;
            }

            nSolarDay -= days[nSolarMonth-1];
            return true;
        }

        internal DateTime LunarToTime(DateTime time, int year, int month, int day) {
            int gy=0;  int gm=0; int gd=0;
            LunarToGregorian(year, month, day, ref gy, ref gm, ref gd);
            return (GregorianCalendar.GetDefaultInstance().ToDateTime(gy,gm,gd,time.Hour,time.Minute,time.Second,time.Millisecond));
        }

        internal void TimeToLunar(DateTime time, ref int year, ref int month, ref int day) {
            int gy=0; int gm=0; int gd=0;

            Calendar Greg = GregorianCalendar.GetDefaultInstance();
            gy = Greg.GetYear(time);
            gm = Greg.GetMonth(time);
            gd = Greg.GetDayOfMonth(time);

            GregorianToLunar(gy, gm, gd, ref year, ref month, ref day);
        }

        // Returns the DateTime resulting from adding the given number of
        // months to the specified DateTime. The result is computed by incrementing
        // (or decrementing) the year and month parts of the specified DateTime by
        // value months, and, if required, adjusting the day part of the
        // resulting date downwards to the last day of the resulting month in the
        // resulting year. The time-of-day part of the result is the same as the
        // time-of-day part of the specified DateTime.
        //

        public override DateTime AddMonths(DateTime time, int months) {
            if (months < -120000 || months > 120000) {
                throw new ArgumentOutOfRangeException(
                            "months",
                            Environment.GetResourceString("ArgumentOutOfRange_Range", -120000, 120000));
            }
            Contract.EndContractBlock();

            CheckTicksRange(time.Ticks);

            int y=0;  int m=0; int d=0;
            TimeToLunar(time, ref y, ref m, ref d);

            int i = m + months;
            if (i > 0) {
                int monthsInYear = InternalIsLeapYear(y)?13:12;

                while (i-monthsInYear > 0) {
                    i -= monthsInYear;
                    y++;
                    monthsInYear = InternalIsLeapYear(y)?13:12;
                }
                m = i;
            } else {
                int monthsInYear;
                while (i <= 0) {
                    monthsInYear = InternalIsLeapYear(y-1)?13:12;
                    i += monthsInYear;
                    y--;
                }
                m = i;
            }

            int days = InternalGetDaysInMonth(y, m);
            if (d > days) {
                d = days;
            }
            DateTime dt = LunarToTime(time, y, m, d);

            CheckAddResult(dt.Ticks, MinSupportedDateTime, MaxSupportedDateTime);
            return (dt);
        }


        public override DateTime AddYears(DateTime time, int years) {
            CheckTicksRange(time.Ticks);

            int y=0;  int m=0; int d=0;
            TimeToLunar(time, ref y, ref m, ref d);

            y += years;

            if (m==13 && !InternalIsLeapYear(y)) {
                m = 12;
                d = InternalGetDaysInMonth(y, m);
            }
            int DaysInMonths = InternalGetDaysInMonth(y, m);
            if (d > DaysInMonths) {
                d = DaysInMonths;
            }

            DateTime dt = LunarToTime(time, y, m, d);
            CheckAddResult(dt.Ticks, MinSupportedDateTime, MaxSupportedDateTime);
            return (dt);
        }

        // Returns the day-of-year part of the specified DateTime. The returned value
        // is an integer between 1 and [354|355 |383|384].
        //

        public override int GetDayOfYear(DateTime time) {
            CheckTicksRange(time.Ticks);

            int y=0;  int m=0; int d=0;
            TimeToLunar(time, ref y, ref m, ref d);

            for (int i=1; i<m ;i++)
            {
                d = d + InternalGetDaysInMonth(y, i);
            }
            return d;
        }

        // Returns the day-of-month part of the specified DateTime. The returned
        // value is an integer between 1 and 29 or 30.
        //

        public override int GetDayOfMonth(DateTime time) {
            CheckTicksRange(time.Ticks);

            int y=0;  int m=0; int d=0;
            TimeToLunar(time, ref y, ref m, ref d);

            return d;
        }

        // Returns the number of days in the year given by the year argument for the current era.
        //

        public override int GetDaysInYear(int year, int era) {
            year = CheckYearRange(year, era);

            int Days = 0;
            int monthsInYear = InternalIsLeapYear(year) ? 13 : 12;

            while (monthsInYear != 0)
                Days += InternalGetDaysInMonth(year, monthsInYear--);

            return Days;
        }

        // Returns the month part of the specified DateTime. The returned value is an
        // integer between 1 and 13.
        //

        public override int GetMonth(DateTime time) {
            CheckTicksRange(time.Ticks);

            int y=0;  int m=0; int d=0;
            TimeToLunar(time, ref y, ref m, ref d);

            return m;
        }

        // Returns the year part of the specified DateTime. The returned value is an
        // integer between 1 and MaxCalendarYear.
        //

        public override int GetYear(DateTime time) {
            CheckTicksRange(time.Ticks);

            int y=0; int m=0; int d=0;
            TimeToLunar(time, ref y, ref m, ref d);

            return GetYear(y, time);
        }

        // Returns the day-of-week part of the specified DateTime. The returned value
        // is an integer between 0 and 6, where 0 indicates Sunday, 1 indicates
        // Monday, 2 indicates Tuesday, 3 indicates Wednesday, 4 indicates
        // Thursday, 5 indicates Friday, and 6 indicates Saturday.
        //

        public override DayOfWeek GetDayOfWeek(DateTime time) {
            CheckTicksRange(time.Ticks);
            return ((DayOfWeek)((int)(time.Ticks / Calendar.TicksPerDay + 1) % 7));
        }

        // Returns the number of months in the specified year and era.

        public override int GetMonthsInYear(int year, int era) {
            year = CheckYearRange(year, era);
            return (InternalIsLeapYear(year)?13:12);
        }

        // Checks whether a given day in the specified era is a leap day. This method returns true if
        // the date is a leap day, or false if not.
        //

        public override bool IsLeapDay(int year, int month, int day, int era) {
            year = CheckYearMonthRange(year, month, era);
            int daysInMonth = InternalGetDaysInMonth(year, month);

            if (day < 1 || day > daysInMonth) {
                throw new ArgumentOutOfRangeException(
                            "day",
                            Environment.GetResourceString("ArgumentOutOfRange_Day", daysInMonth, month));
            }
            int m = GetYearInfo(year, LeapMonth);
            return ((m!=0) && (month == (m+1)));
        }

        // Checks whether a given month in the specified era is a leap month. This method returns true if
        // month is a leap month, or false if not.
        //

        public override bool IsLeapMonth(int year, int month, int era) {
            year = CheckYearMonthRange(year, month, era);
            int m = GetYearInfo(year, LeapMonth);
            return ((m!=0) && (month == (m+1)));
        }

        // Returns  the leap month in a calendar year of the specified era. This method returns 0
        // if this this year is not a leap year.
        //

        public override int  GetLeapMonth(int year, int era) {
            year = CheckYearRange(year, era);
            int month = GetYearInfo(year, LeapMonth);
            if (month>0)
            {
                return (month+1);
            }
            return 0;
        }

        internal bool InternalIsLeapYear(int year) {
            return (GetYearInfo(year, LeapMonth)!=0);
        }
        // Checks whether a given year in the specified era is a leap year. This method returns true if
        // year is a leap year, or false if not.
        //

        public override bool IsLeapYear(int year, int era) {
            year = CheckYearRange(year, era);
            return InternalIsLeapYear(year);
        }

        private const int DEFAULT_GREGORIAN_TWO_DIGIT_YEAR_MAX = 2029;


        public override int TwoDigitYearMax {
            get {
                if (twoDigitYearMax == -1) {
                    twoDigitYearMax = GetSystemTwoDigitYearSetting(BaseCalendarID, GetYear(new DateTime(DEFAULT_GREGORIAN_TWO_DIGIT_YEAR_MAX, 1, 1)));
                }
                return (twoDigitYearMax);
            }

            set {
                VerifyWritable();
                if (value < 99 || value > MaxCalendarYear)
                {
                    throw new ArgumentOutOfRangeException(
                                "value",
                                Environment.GetResourceString("ArgumentOutOfRange_Range", 99, MaxCalendarYear));
                }
                twoDigitYearMax = value;
            }
        }


        public override int ToFourDigitYear(int year) {
            if (year < 0) {
                throw new ArgumentOutOfRangeException("year",
                    Environment.GetResourceString("ArgumentOutOfRange_NeedNonNegNum"));
            }
            Contract.EndContractBlock();

            year = base.ToFourDigitYear(year);
            CheckYearRange(year, CurrentEra);
            return (year);
        }

    }
}
