//
//  Copyright (c) 2009-2011 Artyom Beilis (Tonkikh)
//
//  Distributed under the Boost Software License, Version 1.0. (See
//  accompanying file LICENSE_1_0.txt or copy at
//  http://www.boost.org/LICENSE_1_0.txt)
//
#define BOOST_LOCALE_SOURCE
#include <boost/config.hpp>
#ifdef BOOST_MSVC
#  pragma warning(disable : 4996)
#endif
#include <locale>
#include <string>
#include <ios>
#include <boost/locale/date_time_facet.hpp>
#include <boost/locale/date_time.hpp>
#include <stdlib.h>
#include <ctime>
#include <memory>
#include <algorithm>
#include <limits>

#include "timezone.hpp"
#include "gregorian.hpp"

namespace boost {
namespace locale {
namespace util {
    namespace {

        int is_leap(int year)
        {
            if(year % 400 == 0)
                return 1;
            if(year % 100 == 0)
                return 0;
            if(year % 4 == 0)
                return 1;
            return 0;
        }

        int days_in_month(int year,int month)
        {
            static const int tbl[2][12] = {
                { 31,28,31,30,31,30,31,31,30,31,30,31 },
                { 31,29,31,30,31,30,31,31,30,31,30,31 }
            };
            return tbl[is_leap(year)][month - 1];
        }

        inline int days_from_0(int year)
        {
            year--;
            return 365 * year + (year / 400) - (year/100) + (year / 4);
        }

        int days_from_1970(int year)
        {
            static const int days_from_0_to_1970 = days_from_0(1970);
            return days_from_0(year) - days_from_0_to_1970;
        }

        int days_from_1jan(int year,int month,int day)
        {
            static const int days[2][12] = {
                { 0,31,59,90,120,151,181,212,243,273,304,334 },
                { 0,31,60,91,121,152,182,213,244,274,305,335 }
            };
            return days[is_leap(year)][month-1] + day - 1;
        }
        
        std::time_t internal_timegm(std::tm const *t)
        {
            int year = t->tm_year + 1900;
            int month = t->tm_mon;
            if(month > 11) {
                year += month/12;
                month %= 12;
            }
            else if(month < 0) {
                int years_diff = (-month + 11)/12;
                year -= years_diff;
                month+=12 * years_diff;
            }
            month++;
            int day = t->tm_mday;
            int day_of_year = days_from_1jan(year,month,day);
            int days_since_epoch = days_from_1970(year) + day_of_year;
            
            std::time_t seconds_in_day = 3600 * 24;
            std::time_t result =  seconds_in_day * days_since_epoch + 3600 * t->tm_hour + 60 * t->tm_min + t->tm_sec;
            
            return result;
        }

    } // anon




    namespace {

        // Locale dependent data

        bool comparator(char const *left,char const *right)
        {
            return strcmp(left,right) < 0;
        }
        
        //
        // Ref: CLDR 1.9 common/supplemental/supplementalData.xml
        //
        // monday - default
        // fri - MV
        // sat - AE AF BH DJ DZ EG ER ET IQ IR JO KE KW LY MA OM QA SA SD SO SY TN YE
        // sun - AR AS AZ BW CA CN FO GE GL GU HK IL IN JM JP KG KR LA MH MN MO MP MT NZ PH PK SG TH TT TW UM US UZ VI ZW
        //

        int first_day_of_week(char const *terr) {
            static char const * const sat[] = {
                "AE","AF","BH","DJ","DZ","EG","ER","ET","IQ","IR",
                "JO","KE","KW","LY","MA","OM","QA","SA","SD","SO",
                "SY","TN","YE"
            };
            // workaround for Sun Solaris !@#%@#$%@#$%234
            #ifdef sun
            #undef sun
            #endif
            static char const * const sun[] = {
                "AR","AS","AZ","BW","CA","CN","FO","GE","GL","GU",
                "HK","IL","IN","JM","JP","KG","KR","LA","MH","MN",
                "MO","MP","MT","NZ","PH","PK","SG","TH","TT","TW",
                "UM","US","UZ","VI","ZW" 
            };
            if(strcmp(terr,"MV") == 0)
                return 5; // fri
            if(std::binary_search<char const * const *>(sat,sat+sizeof(sat)/(sizeof(sat[0])),terr,comparator))
                return 6; // sat
            if(std::binary_search<char const * const *>(sun,sun+sizeof(sun)/(sizeof(sun[0])),terr,comparator))
                return 0; // sun
            // default
            return 1; // mon
        }
    }

    class gregorian_calendar : public abstract_calendar {
    public:
            
            gregorian_calendar(std::string const &terr)
            {
                first_day_of_week_ = first_day_of_week(terr.c_str());
                time_ = std::time(0);
                is_local_ = true;
                tzoff_ = 0;
                from_time(time_);
            }
                
            ///
            /// Make a polymorphic copy of the calendar
            ///
            virtual gregorian_calendar *clone() const
            {
                return new gregorian_calendar(*this);
            }

            ///
            /// Set specific \a value for period \a p, note not all values are settable.
            ///
            virtual void set_value(period::marks::period_mark p,int value) 
            {
                using namespace period::marks;
                switch(p) {
                case era:                        ///< Era i.e. AC, BC in Gregorian and Julian calendar, range [0,1]
                    return;
                case year:                       ///< Year, it is calendar specific
                case extended_year:              ///< Extended year for Gregorian/Julian calendars, where 1 BC == 0, 2 BC == -1.
                    tm_updated_.tm_year = value - 1900;
                    break;
                case month:
                    tm_updated_.tm_mon = value;
                    break;
                case day:
                    tm_updated_.tm_mday = value;
                    break;
                case hour:                       ///< 24 clock hour [0..23]
                    tm_updated_.tm_hour = value;
                    break;
                case hour_12:                    ///< 12 clock hour [0..11]
                    tm_updated_.tm_hour = tm_updated_.tm_hour / 12 * 12 + value;
                    break;
                case am_pm:                      ///< am or pm marker, [0..1]
                    tm_updated_.tm_hour = 12 * value + tm_updated_.tm_hour % 12;
                    break;
                case minute:                     ///< minute [0..59]
                    tm_updated_.tm_min = value;
                    break;
                case second:
                    tm_updated_.tm_sec = value;
                    break;
                case day_of_year:
                    normalize();
                    tm_updated_.tm_mday += (value - (tm_updated_.tm_yday + 1));
                    break;
                case day_of_week:           ///< Day of week, starting from Sunday, [1..7]
                    if(value < 1) // make sure it is positive 
                        value += (-value / 7) * 7 + 7;
                    // convert to local DOW
                    value = (value - 1 - first_day_of_week_ + 14) % 7 + 1;
                    // fall throght
                case day_of_week_local:     ///< Local day of week, for example in France Monday is 1, in US Sunday is 1, [1..7]
                    normalize();
                    tm_updated_.tm_mday += (value - 1) - (tm_updated_.tm_wday - first_day_of_week_ + 7) % 7;
                    break;
                case day_of_week_in_month:  ///< Original number of the day of the week in month. (1st sunday, 2nd sunday etc)
                case week_of_year:          ///< The week number in the year, 4 is the minimal number of days to be in month
                case week_of_month:         ///< The week number withing current month
                    {
                        normalize();
                        int current_week = get_value(p,current);
                        int diff = 7 * (value - current_week);
                        tm_updated_.tm_mday += diff;
                    }
                    break;
                case period::marks::first_day_of_week:          ///< For example Sunday in US, Monday in France
                default:
                    return;
                }
                normalized_ = false;
            }

            void normalize()
            {
                if(!normalized_) {
                    std::tm val = tm_updated_;
                    val.tm_isdst = -1;
                    val.tm_wday = -1; // indecator of error
                    std::time_t point = -1;
                    if(is_local_) {
                        point = std::mktime(&val);
                        if(point == static_cast<std::time_t>(-1)){
                            #ifndef BOOST_WINDOWS
                            // windows does not handle negative time_t, under other plaforms
                            // it may be actually valid value in  1969-12-31 23:59:59
                            // so we check that a filed was updated - does not happen in case of error
                            if(val.tm_wday == -1)
                            #endif
                            {
                                throw date_time_error("boost::locale::gregorian_calendar: invalid time");
                            }
                        }
                    }
                    else {
                        point = internal_timegm(&val);
                        #ifdef BOOST_WINDOWS
                        // Windows uses TLS, thread safe
                        std::tm *revert_point = 0;
                        if(point < 0  || (revert_point = gmtime(&point)) == 0)
                            throw date_time_error("boost::locale::gregorian_calendar time is out of range");
                        val = *revert_point;
                        #else
                        if(!gmtime_r(&point,&val))
                            throw date_time_error("boost::locale::gregorian_calendar invalid time");
                        #endif
                        
                    }
                    
                    time_ = point - tzoff_;
                    tm_ = val;
                    tm_updated_ = val;
                    normalized_ = true;
                }
            }

            int get_week_number(int day,int wday) const
            {
                ///
                /// This is the number of days that are considered withing
                /// period such that the week belongs there
                ///
                static const int days_in_full_week = 4;


                // Alaways use local week start
                int current_dow = (wday - first_day_of_week_ + 7) % 7;
                // Calculate local week day of Jan 1st.
                int first_week_day = (current_dow + 700 - day) % 7; 
                    // adding something big devidable by 7

                int start_of_period_in_weeks;
                if(first_week_day < days_in_full_week) {
                    start_of_period_in_weeks = - first_week_day;
                }
                else {
                    start_of_period_in_weeks = 7 - first_week_day;
                }
                int week_number_in_days = day - start_of_period_in_weeks;
                if(week_number_in_days < 0)
                    return -1;
                return week_number_in_days / 7 + 1;
            }

            ///
            /// Get specific value for period \a p according to a value_type \a v
            ///
            virtual int get_value(period::marks::period_mark p,value_type v) const 
            {
                using namespace period::marks;
                switch(p) {
                case era:
                    return 1;
                case year:
                case extended_year:
                    switch(v) {
                    case absolute_minimum:
                    case greatest_minimum:
                    case actual_minimum:
                        #ifdef BOOST_WINDOWS
                        return 1970; // Unix epoch windows can't handle negative time_t
                        #else
                        if(sizeof(std::time_t) == 4)
                            return 1901; // minimal year with 32 bit time_t
                        else
                            return 1; 
                        #endif
                    case absolute_maximum:
                    case least_maximum:
                    case actual_maximum:
                        if(sizeof(std::time_t) == 4)
                            return 2038; // Y2K38 - maximal with 32 bit time_t
                        else
                            return std::numeric_limits<int>::max();
                    case current:
                        return tm_.tm_year + 1900;
                    };
                    break;
                case month:
                    switch(v) {
                    case absolute_minimum:
                    case greatest_minimum:
                    case actual_minimum:
                        return 0;
                    case absolute_maximum:
                    case least_maximum:
                    case actual_maximum:
                        return 11;
                    case current:
                        return tm_.tm_mon;
                    };
                    break;
                case day:
                    switch(v) {
                    case absolute_minimum:
                    case greatest_minimum:
                    case actual_minimum:
                        return 1;
                    case absolute_maximum:
                        return 31;
                    case least_maximum:
                        return 28;
                    case actual_maximum:
                        return days_in_month(tm_.tm_year + 1900,tm_.tm_mon + 1);
                    case current:
                        return tm_.tm_mday;
                    };
                    break;
                case day_of_year:                ///< The number of day in year, starting from 1
                    switch(v) {
                    case absolute_minimum:
                    case greatest_minimum:
                    case actual_minimum:
                        return 1;
                    case absolute_maximum:
                        return 366;
                    case least_maximum:
                        return 365;
                    case actual_maximum:
                        return is_leap(tm_.tm_year + 1900) ? 366 : 365;
                    case current:
                        return tm_.tm_yday + 1;
                    }
                    break;
                case day_of_week:                ///< Day of week, starting from Sunday, [1..7]
                    switch(v) {
                    case absolute_minimum:
                    case greatest_minimum:
                    case actual_minimum:
                        return 1;
                    case absolute_maximum:
                    case least_maximum:
                    case actual_maximum:
                        return 7;
                    case current:
                        return tm_.tm_wday + 1;
                    }
                    break;
                case day_of_week_local:     ///< Local day of week, for example in France Monday is 1, in US Sunday is 1, [1..7]
                    switch(v) {
                    case absolute_minimum:
                    case greatest_minimum:
                    case actual_minimum:
                        return 1;
                    case absolute_maximum:
                    case least_maximum:
                    case actual_maximum:
                        return 7;
                    case current:
                        return (tm_.tm_wday - first_day_of_week_ + 7) % 7 + 1;
                    }
                    break;
                case hour:                       ///< 24 clock hour [0..23]
                    switch(v) {
                    case absolute_minimum:
                    case greatest_minimum:
                    case actual_minimum:
                        return 0;
                    case absolute_maximum:
                    case least_maximum:
                    case actual_maximum:
                        return 23;
                    case current:
                        return tm_.tm_hour;
                    }
                    break;
                case hour_12:                    ///< 12 clock hour [0..11]
                    switch(v) {
                    case absolute_minimum:
                    case greatest_minimum:
                    case actual_minimum:
                        return 0;
                    case absolute_maximum:
                    case least_maximum:
                    case actual_maximum:
                        return 11;
                    case current:
                        return tm_.tm_hour % 12;
                    }
                    break;
                case am_pm:                      ///< am or pm marker, [0..1]
                    switch(v) {
                    case absolute_minimum:
                    case greatest_minimum:
                    case actual_minimum:
                        return 0;
                    case absolute_maximum:
                    case least_maximum:
                    case actual_maximum:
                        return 1;
                    case current:
                        return tm_.tm_hour >= 12 ? 1 : 0;
                    }
                    break;
                case minute:                     ///< minute [0..59]
                    switch(v) {
                    case absolute_minimum:
                    case greatest_minimum:
                    case actual_minimum:
                        return 0;
                    case absolute_maximum:
                    case least_maximum:
                    case actual_maximum:
                        return 59;
                    case current:
                        return tm_.tm_min;
                    }
                    break;
                case second:                     ///< second [0..59]
                    switch(v) {
                    case absolute_minimum:
                    case greatest_minimum:
                    case actual_minimum:
                        return 0;
                    case absolute_maximum:
                    case least_maximum:
                    case actual_maximum:
                        return 59;
                    case current:
                        return tm_.tm_sec;
                    }
                    break;
                case period::marks::first_day_of_week:          ///< For example Sunday in US, Monday in France
                    return first_day_of_week_ + 1;
                
                case week_of_year:               ///< The week number in the year
                    switch(v) {
                    case absolute_minimum:
                    case greatest_minimum:
                    case actual_minimum:
                        return 1;
                    case absolute_maximum:
                        return 53;
                    case least_maximum:
                        return 52;
                    case actual_maximum:
                        {
                            int year = tm_.tm_year + 1900;
                            int end_of_year_days = (is_leap(year) ? 366 : 365) - 1;
                            int dow_of_end_of_year = (end_of_year_days - tm_.tm_yday + tm_.tm_wday) % 7;
                            return get_week_number(end_of_year_days,dow_of_end_of_year);
                        }
                    case current:
                        {
                            int val = get_week_number(tm_.tm_yday,tm_.tm_wday);
                            if(val < 0)
                                return 53;
                            return val;
                        }
                    }
                case week_of_month:              ///< The week number withing current month
                    switch(v) {
                    case absolute_minimum:
                    case greatest_minimum:
                    case actual_minimum:
                        return 1;
                    case absolute_maximum:
                        return 5;
                    case least_maximum:
                        return 4;
                    case actual_maximum:
                        {
                            int end_of_month_days = days_in_month(tm_.tm_year + 1900,tm_.tm_mon + 1);
                            int dow_of_end_of_month = (end_of_month_days - tm_.tm_mday + tm_.tm_wday) % 7;
                            return get_week_number(end_of_month_days,dow_of_end_of_month);
                        }
                    case current:
                        {
                            int val = get_week_number(tm_.tm_mday,tm_.tm_wday);
                            if(val < 0)
                                return 5;
                            return val;
                        }
                    }

                case day_of_week_in_month:       ///< Original number of the day of the week in month.
                    switch(v) {
                    case absolute_minimum:
                    case greatest_minimum:
                    case actual_minimum:
                        return 1;
                    case absolute_maximum:
                        return 5;
                    case least_maximum:
                        return 4;
                    case actual_maximum:
                        if(tm_.tm_mon == 1 && !is_leap(tm_.tm_year + 1900)) {
                            // only in february in non leap year is 28 days, the rest
                            // conver more then 4 weeks
                            return 4;
                        }
                        return 5;
                    case current:
                        return (tm_.tm_mday - 1) / 7 + 1;
                    default:
                        ;
                    }
                default:
                    ;
                }
                return 0;
 
            }

            ///
            /// Set current time point
            ///
            virtual void set_time(posix_time const &p)
            {
                from_time(static_cast<std::time_t>(p.seconds));
            }
            virtual posix_time get_time() const  
            {
                posix_time pt = { time_, 0};
                return pt;
            }

            ///
            /// Set option for calendar, for future use
            ///
            virtual void set_option(calendar_option_type opt,int /*v*/)
            {
                switch(opt) {
                case is_gregorian:
                    throw date_time_error("is_gregorian is not settable options for calendar");
                case is_dst:
                    throw date_time_error("is_dst is not settable options for calendar");
                default:
                    ;
                }
            }
            ///
            /// Get option for calendar, currently only check if it is Gregorian calendar
            ///
            virtual int get_option(calendar_option_type opt) const 
            {
                switch(opt) {
                case is_gregorian:
                    return 1;
                case is_dst:
                    return tm_.tm_isdst == 1;
                default:
                    return 0;
                };
            }

            ///
            /// Adjust period's \a p value by \a difference items using a update_type \a u.
            /// Note: not all values are adjustable
            ///
            virtual void adjust_value(period::marks::period_mark p,update_type u,int difference)
            {
                switch(u) {
                case move:
                    {
                        using namespace period::marks;
                        switch(p) {
                        case year:                       ///< Year, it is calendar specific
                        case extended_year:              ///< Extended year for Gregorian/Julian calendars, where 1 BC == 0, 2 BC == -1.
                            tm_updated_.tm_year +=difference;
                            break;
                        case month:
                            tm_updated_.tm_mon +=difference;
                            break;
                        case day:
                        case day_of_year:
                        case day_of_week:                ///< Day of week, starting from Sunday, [1..7]
                        case day_of_week_local: ///< Local day of week, for example in France Monday is 1, in US Sunday is 1, [1..7]
                            tm_updated_.tm_mday +=difference;
                            break;
                        case hour:                       ///< 24 clock hour [0..23]
                        case hour_12:                    ///< 12 clock hour [0..11]
                            tm_updated_.tm_hour += difference;
                            break;
                        case am_pm:                      ///< am or pm marker, [0..1]
                            tm_updated_.tm_hour += 12 * difference;
                            break;
                        case minute:                     ///< minute [0..59]
                            tm_updated_.tm_min += difference;
                            break;
                        case second:
                            tm_updated_.tm_sec += difference;
                            break;
                        case week_of_year:               ///< The week number in the year
                        case week_of_month:              ///< The week number withing current month
                        case day_of_week_in_month:       ///< Original number of the day of the week in month.
                            tm_updated_.tm_mday +=difference * 7;
                            break;
                        default:
                            ; // Not all values are adjustable
                        }
                        normalized_ = false;
                        normalize();
                    }
                    break;
                case roll:
                    { // roll
                        int cur_min = get_value(p,actual_minimum);
                        int cur_max = get_value(p,actual_maximum);
                        int max_diff = cur_max - cur_min + 1;
                        if(max_diff > 0) {
                            int value = get_value(p,current);
                            int addon = 0;
                            if(difference < 0)
                                addon = ((-difference/max_diff) + 1) * max_diff;
                            value = (value - cur_min + difference + addon) % max_diff + cur_min;
                            set_value(p,value);
                            normalize();
                        }
                    }
                default:
                    ;
                }
            }

            int get_diff(period::marks::period_mark p,int diff,gregorian_calendar const *other) const
            {
                if(diff == 0)
                    return 0;
                std::auto_ptr<gregorian_calendar> self(clone());
                self->adjust_value(p,move,diff);
                if(diff > 0){
                    if(self->time_ > other->time_)
                        return diff - 1;
                    else
                        return diff;
                }
                else {
                    if(self->time_ < other->time_)
                        return diff + 1;
                    else
                        return diff;
                }
             }

            ///
            /// Calculate the difference between this calendar  and \a other in \a p units
            ///
            virtual int difference(abstract_calendar const *other_cal,period::marks::period_mark p) const 
            {
                std::auto_ptr<gregorian_calendar> keeper;
                gregorian_calendar const *other = dynamic_cast<gregorian_calendar const *>(other_cal);
                if(!other) {
                    keeper.reset(clone());
                    keeper->set_time(other_cal->get_time());
                    other = keeper.get();
                }

                int factor = 1; // for weeks vs days handling

                using namespace period::marks;
                switch(p) {
                case era:
                    return 0;
                case year:
                case extended_year:
                    {
                        int diff = other->tm_.tm_year - tm_.tm_year;
                        return get_diff(period::marks::year,diff,other);
                    }
                case month:
                    {
                        int diff = 12 * (other->tm_.tm_year - tm_.tm_year) 
                                    + other->tm_.tm_mon - tm_.tm_mon;
                        return get_diff(period::marks::month,diff,other);
                    }
                case day_of_week_in_month:
                case week_of_month:
                case week_of_year:
                    factor = 7;
                    // fall 
                case day:
                case day_of_year:
                case day_of_week:
                case day_of_week_local:
                    {
                        int diff = other->tm_.tm_yday - tm_.tm_yday;
                        if(other->tm_.tm_year != tm_.tm_year) {
                            diff += days_from_0(other->tm_.tm_year + 1900) -
                                    days_from_0(tm_.tm_year + 1900);
                        }
                        return get_diff(period::marks::day,diff,other) / factor;
                    }
                case am_pm:
                    return static_cast<int>( (other->time_ - time_) / (3600*12) );
                case hour:
                case hour_12:
                    return static_cast<int>( (other->time_ - time_) / 3600 );
                case minute:
                    return static_cast<int>( (other->time_ - time_) / 60 );
                case second:
                    return static_cast<int>( other->time_ - time_  );
                default:
                    return 0;
                };
            }

            ///
            /// Set time zone, empty - use system
            ///
            virtual void set_timezone(std::string const &tz)
            {
                if(tz.empty()) {
                    is_local_ = true;
                    tzoff_ = 0;
                }
                else {
                    is_local_ = false;
                    tzoff_ = parse_tz(tz);
                }
                from_time(time_);
                time_zone_name_ = tz;
            }
            virtual std::string get_timezone() const
            {
                return time_zone_name_;
            }

            virtual bool same(abstract_calendar const *other) const 
            {
                gregorian_calendar const *gcal = dynamic_cast<gregorian_calendar const *>(other);
                if(!gcal)
                    return false;
                return 
                    gcal->tzoff_ == tzoff_ 
                    && gcal->is_local_ == is_local_
                    && gcal->first_day_of_week_  == first_day_of_week_;
            }

            virtual ~gregorian_calendar()
            {
            }

    private:

        void from_time(std::time_t point)
        {
            std::time_t real_point = point + tzoff_;
            std::tm *t = 0;
            #ifdef BOOST_WINDOWS
            // Windows uses TLS, thread safe
            t = is_local_ ? localtime(&real_point) : gmtime(&real_point);
            #else
            std::tm tmp_tm;
            t = is_local_ ? localtime_r(&real_point,&tmp_tm) : gmtime_r(&real_point,&tmp_tm);
            #endif
            if(!t) {
                throw date_time_error("boost::locale::gregorian_calendar: invalid time point");
            }
            tm_ = *t;
            tm_updated_ = *t;
            normalized_ = true;
            time_ = point;
        }
        int first_day_of_week_;
        std::time_t time_;
        std::tm tm_;
        std::tm tm_updated_;
        bool normalized_;
        bool is_local_;
        int tzoff_;
        std::string time_zone_name_;
        
    };
    
    abstract_calendar *create_gregorian_calendar(std::string const &terr)
    {
        return new gregorian_calendar(terr);
    }

    class gregorian_facet : public calendar_facet {
    public:
        gregorian_facet(std::string const &terr,size_t refs = 0) : 
            calendar_facet(refs),
            terr_(terr)
        {
        }
        virtual abstract_calendar *create_calendar() const 
        {
            return create_gregorian_calendar(terr_);
        }
    private:
        std::string terr_;
    };
    
    std::locale install_gregorian_calendar(std::locale const &in,std::string const &terr)
    {
        return std::locale(in,new gregorian_facet(terr));
    }


} // util
} // locale 
} //boost


// vim: tabstop=4 expandtab shiftwidth=4 softtabstop=4
