// © 2016 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html
/*
*******************************************************************************
* Copyright (C) 2007-2012, International Business Machines Corporation and
* others. All Rights Reserved.
*******************************************************************************
*/

#include "utypeinfo.h"  // for 'typeid' to work

#include "unicode/utypes.h"

#if !UCONFIG_NO_FORMATTING

#include "unicode/tzrule.h"
#include "unicode/ucal.h"
#include "gregoimp.h"
#include "cmemory.h"
#include "uarrsort.h"

U_CDECL_BEGIN
// UComparator function for sorting start times
static int32_t U_CALLCONV
compareDates(const void * /*context*/, const void *left, const void *right) {
    UDate l = *((UDate*)left);
    UDate r = *((UDate*)right);
    int32_t res = l < r ? -1 : (l == r ? 0 : 1);
    return res;
}
U_CDECL_END

U_NAMESPACE_BEGIN

TimeZoneRule::TimeZoneRule(const UnicodeString& name, int32_t rawOffset, int32_t dstSavings)
: UObject(), fName(name), fRawOffset(rawOffset), fDSTSavings(dstSavings) {
}

TimeZoneRule::TimeZoneRule(const TimeZoneRule& source)
: UObject(source), fName(source.fName), fRawOffset(source.fRawOffset), fDSTSavings(source.fDSTSavings) {
}

TimeZoneRule::~TimeZoneRule() {
}

TimeZoneRule&
TimeZoneRule::operator=(const TimeZoneRule& right) {
    if (this != &right) {
        fName = right.fName;
        fRawOffset = right.fRawOffset;
        fDSTSavings = right.fDSTSavings;
    }
    return *this;
}

bool
TimeZoneRule::operator==(const TimeZoneRule& that) const {
    return ((this == &that) ||
            (typeid(*this) == typeid(that) &&
            fName == that.fName &&
            fRawOffset == that.fRawOffset &&
            fDSTSavings == that.fDSTSavings));
}

bool
TimeZoneRule::operator!=(const TimeZoneRule& that) const {
    return !operator==(that);
}

UnicodeString&
TimeZoneRule::getName(UnicodeString& name) const {
    name = fName;
    return name;
}

int32_t
TimeZoneRule::getRawOffset() const {
    return fRawOffset;
}

int32_t
TimeZoneRule::getDSTSavings() const {
    return fDSTSavings;
}

UBool
TimeZoneRule::isEquivalentTo(const TimeZoneRule& other) const {
    return ((this == &other) ||
            (typeid(*this) == typeid(other) &&
            fRawOffset == other.fRawOffset &&
            fDSTSavings == other.fDSTSavings));
}


UOBJECT_DEFINE_RTTI_IMPLEMENTATION(InitialTimeZoneRule)

InitialTimeZoneRule::InitialTimeZoneRule(const UnicodeString& name,
                                         int32_t rawOffset,
                                         int32_t dstSavings)
: TimeZoneRule(name, rawOffset, dstSavings) {
}

InitialTimeZoneRule::InitialTimeZoneRule(const InitialTimeZoneRule& source)
: TimeZoneRule(source) {
}

InitialTimeZoneRule::~InitialTimeZoneRule() {
}

InitialTimeZoneRule*
InitialTimeZoneRule::clone() const {
    return new InitialTimeZoneRule(*this);
}

InitialTimeZoneRule&
InitialTimeZoneRule::operator=(const InitialTimeZoneRule& right) {
    if (this != &right) {
        TimeZoneRule::operator=(right);
    }
    return *this;
}

bool
InitialTimeZoneRule::operator==(const TimeZoneRule& that) const {
    return ((this == &that) ||
            (typeid(*this) == typeid(that) &&
            TimeZoneRule::operator==(that)));
}

bool
InitialTimeZoneRule::operator!=(const TimeZoneRule& that) const {
    return !operator==(that);
}

UBool
InitialTimeZoneRule::isEquivalentTo(const TimeZoneRule& other) const {
    if (this == &other) {
        return true;
    }
    if (typeid(*this) != typeid(other) || TimeZoneRule::isEquivalentTo(other) == false) {
        return false;
    }
    return true;
}

UBool
InitialTimeZoneRule::getFirstStart(int32_t /*prevRawOffset*/,
                                  int32_t /*prevDSTSavings*/,
                                  UDate& /*result*/) const {
    return false;
}

UBool
InitialTimeZoneRule::getFinalStart(int32_t /*prevRawOffset*/,
                                  int32_t /*prevDSTSavings*/,
                                  UDate& /*result*/) const {
    return false;
}

UBool
InitialTimeZoneRule::getNextStart(UDate /*base*/,
                                 int32_t /*prevRawOffset*/,
                                 int32_t /*prevDSTSavings*/,
                                 UBool /*inclusive*/,
                                 UDate& /*result*/) const {
    return false;
}

UBool
InitialTimeZoneRule::getPreviousStart(UDate /*base*/,
                                     int32_t /*prevRawOffset*/,
                                     int32_t /*prevDSTSavings*/,
                                     UBool /*inclusive*/,
                                     UDate& /*result*/) const {
    return false;
}


UOBJECT_DEFINE_RTTI_IMPLEMENTATION(AnnualTimeZoneRule)

const int32_t AnnualTimeZoneRule::MAX_YEAR = 0x7FFFFFFF; /* max signed int32 */

AnnualTimeZoneRule::AnnualTimeZoneRule(const UnicodeString& name,
                                       int32_t rawOffset,
                                       int32_t dstSavings, 
                                       const DateTimeRule& dateTimeRule,
                                       int32_t startYear,
                                       int32_t endYear)
: TimeZoneRule(name, rawOffset, dstSavings), fDateTimeRule(new DateTimeRule(dateTimeRule)),
  fStartYear(startYear), fEndYear(endYear) {
}

AnnualTimeZoneRule::AnnualTimeZoneRule(const UnicodeString& name,
                                       int32_t rawOffset,
                                       int32_t dstSavings, 
                                       DateTimeRule* dateTimeRule,
                                       int32_t startYear,
                                       int32_t endYear)
: TimeZoneRule(name, rawOffset, dstSavings), fDateTimeRule(dateTimeRule),
  fStartYear(startYear), fEndYear(endYear) {
}

AnnualTimeZoneRule::AnnualTimeZoneRule(const AnnualTimeZoneRule& source)
: TimeZoneRule(source), fDateTimeRule(new DateTimeRule(*(source.fDateTimeRule))),
  fStartYear(source.fStartYear), fEndYear(source.fEndYear) {
}

AnnualTimeZoneRule::~AnnualTimeZoneRule() {
    delete fDateTimeRule;
}

AnnualTimeZoneRule*
AnnualTimeZoneRule::clone() const {
    return new AnnualTimeZoneRule(*this);
}

AnnualTimeZoneRule&
AnnualTimeZoneRule::operator=(const AnnualTimeZoneRule& right) {
    if (this != &right) {
        TimeZoneRule::operator=(right);
        delete fDateTimeRule;
        fDateTimeRule = right.fDateTimeRule->clone();
        fStartYear = right.fStartYear;
        fEndYear = right.fEndYear;
    }
    return *this;
}

bool
AnnualTimeZoneRule::operator==(const TimeZoneRule& that) const {
    if (this == &that) {
        return true;
    }
    if (typeid(*this) != typeid(that)) {
        return false;
    }
    AnnualTimeZoneRule *atzr = (AnnualTimeZoneRule*)&that;
    return (*fDateTimeRule == *(atzr->fDateTimeRule) &&
            fStartYear == atzr->fStartYear &&
            fEndYear == atzr->fEndYear);
}

bool
AnnualTimeZoneRule::operator!=(const TimeZoneRule& that) const {
    return !operator==(that);
}

const DateTimeRule*
AnnualTimeZoneRule::getRule() const {
    return fDateTimeRule;
}

int32_t
AnnualTimeZoneRule::getStartYear() const {
    return fStartYear;
}

int32_t
AnnualTimeZoneRule::getEndYear() const {
    return fEndYear;
}

UBool
AnnualTimeZoneRule::getStartInYear(int32_t year,
                                   int32_t prevRawOffset,
                                   int32_t prevDSTSavings,
                                   UDate &result) const {
    if (year < fStartYear || year > fEndYear) {
        return false;
    }
    double ruleDay;
    DateTimeRule::DateRuleType type = fDateTimeRule->getDateRuleType();
    if (type == DateTimeRule::DOM) {
        ruleDay = Grego::fieldsToDay(year, fDateTimeRule->getRuleMonth(), fDateTimeRule->getRuleDayOfMonth());
    } else {
        UBool after = true;
        if (type == DateTimeRule::DOW) {
            // Normalize DOW rule into DOW_GEQ_DOM or DOW_LEQ_DOM
            int32_t weeks = fDateTimeRule->getRuleWeekInMonth();
            if (weeks > 0) {
                ruleDay = Grego::fieldsToDay(year, fDateTimeRule->getRuleMonth(), 1);
                ruleDay += 7 * (weeks - 1);
            } else {
                after = false;
                ruleDay = Grego::fieldsToDay(year, fDateTimeRule->getRuleMonth(),
                    Grego::monthLength(year, fDateTimeRule->getRuleMonth()));
                ruleDay += 7 * (weeks + 1);
           }
        } else {
            int32_t month = fDateTimeRule->getRuleMonth();
            int32_t dom = fDateTimeRule->getRuleDayOfMonth();
            if (type == DateTimeRule::DOW_LEQ_DOM) {
                after = false;
                // Handle Feb <=29
                if (month == UCAL_FEBRUARY && dom == 29 && !Grego::isLeapYear(year)) {
                    dom--;
                }
            }
            ruleDay = Grego::fieldsToDay(year, month, dom);
        }
        int32_t dow = Grego::dayOfWeek(ruleDay);
        int32_t delta = fDateTimeRule->getRuleDayOfWeek() - dow;
        if (after) {
            delta = delta < 0 ? delta + 7 : delta;
        } else {
            delta = delta > 0 ? delta - 7 : delta;
        }
        ruleDay += delta;
    }

    result = ruleDay*U_MILLIS_PER_DAY + fDateTimeRule->getRuleMillisInDay();
    if (fDateTimeRule->getTimeRuleType() != DateTimeRule::UTC_TIME) {
        result -= prevRawOffset;
    }
    if (fDateTimeRule->getTimeRuleType() == DateTimeRule::WALL_TIME) {
        result -= prevDSTSavings;
    }
    return true;
}

UBool
AnnualTimeZoneRule::isEquivalentTo(const TimeZoneRule& other) const {
    if (this == &other) {
        return true;
    }
    if (typeid(*this) != typeid(other) || TimeZoneRule::isEquivalentTo(other) == false) {
        return false;
    }
    AnnualTimeZoneRule* that = (AnnualTimeZoneRule*)&other;
    return (*fDateTimeRule == *(that->fDateTimeRule) &&
            fStartYear == that->fStartYear &&
            fEndYear == that->fEndYear);
}

UBool
AnnualTimeZoneRule::getFirstStart(int32_t prevRawOffset,
                                  int32_t prevDSTSavings,
                                  UDate& result) const {
    return getStartInYear(fStartYear, prevRawOffset, prevDSTSavings, result);
}

UBool
AnnualTimeZoneRule::getFinalStart(int32_t prevRawOffset,
                                  int32_t prevDSTSavings,
                                  UDate& result) const {
    if (fEndYear == MAX_YEAR) {
        return false;
    }
    return getStartInYear(fEndYear, prevRawOffset, prevDSTSavings, result);
}

UBool
AnnualTimeZoneRule::getNextStart(UDate base,
                                 int32_t prevRawOffset,
                                 int32_t prevDSTSavings,
                                 UBool inclusive,
                                 UDate& result) const {
    int32_t year, month, dom, dow, doy, mid;
    Grego::timeToFields(base, year, month, dom, dow, doy, mid);
    if (year < fStartYear) {
        return getFirstStart(prevRawOffset, prevDSTSavings, result);
    }
    UDate tmp;
    if (getStartInYear(year, prevRawOffset, prevDSTSavings, tmp)) {
        if (tmp < base || (!inclusive && (tmp == base))) {
            // Return the next one
            return getStartInYear(year + 1, prevRawOffset, prevDSTSavings, result);
        } else {
            result = tmp;
            return true;
        }
    }
    return false;
}

UBool
AnnualTimeZoneRule::getPreviousStart(UDate base,
                                     int32_t prevRawOffset,
                                     int32_t prevDSTSavings,
                                     UBool inclusive,
                                     UDate& result) const {
    int32_t year, month, dom, dow, doy, mid;
    Grego::timeToFields(base, year, month, dom, dow, doy, mid);
    if (year > fEndYear) {
        return getFinalStart(prevRawOffset, prevDSTSavings, result);
    }
    UDate tmp;
    if (getStartInYear(year, prevRawOffset, prevDSTSavings, tmp)) {
        if (tmp > base || (!inclusive && (tmp == base))) {
            // Return the previous one
            return getStartInYear(year - 1, prevRawOffset, prevDSTSavings, result);
        } else {
            result = tmp;
            return true;
        }
    }
    return false;
}

UOBJECT_DEFINE_RTTI_IMPLEMENTATION(TimeArrayTimeZoneRule)

TimeArrayTimeZoneRule::TimeArrayTimeZoneRule(const UnicodeString& name,
                                             int32_t rawOffset,
                                             int32_t dstSavings,
                                             const UDate* startTimes,
                                             int32_t numStartTimes,
                                             DateTimeRule::TimeRuleType timeRuleType)
: TimeZoneRule(name, rawOffset, dstSavings), fTimeRuleType(timeRuleType),
  fStartTimes(nullptr) {
    UErrorCode status = U_ZERO_ERROR;
    initStartTimes(startTimes, numStartTimes, status);
    //TODO - status?
}


TimeArrayTimeZoneRule::TimeArrayTimeZoneRule(const TimeArrayTimeZoneRule& source)
: TimeZoneRule(source), fTimeRuleType(source.fTimeRuleType), fStartTimes(nullptr) {
    UErrorCode status = U_ZERO_ERROR;
    initStartTimes(source.fStartTimes, source.fNumStartTimes, status);
    //TODO - status?
}


TimeArrayTimeZoneRule::~TimeArrayTimeZoneRule() {
    if (fStartTimes != nullptr && fStartTimes != fLocalStartTimes) {
        uprv_free(fStartTimes);
    }
}

TimeArrayTimeZoneRule*
TimeArrayTimeZoneRule::clone() const {
    return new TimeArrayTimeZoneRule(*this);
}


TimeArrayTimeZoneRule&
TimeArrayTimeZoneRule::operator=(const TimeArrayTimeZoneRule& right) {
    if (this != &right) {
        TimeZoneRule::operator=(right);
        UErrorCode status = U_ZERO_ERROR;
        initStartTimes(right.fStartTimes, right.fNumStartTimes, status);
        //TODO - status?
        fTimeRuleType = right.fTimeRuleType;        
    }
    return *this;
}

bool
TimeArrayTimeZoneRule::operator==(const TimeZoneRule& that) const {
    if (this == &that) {
        return true;
    }
    if (typeid(*this) != typeid(that) || !TimeZoneRule::operator==(that)) {
        return false;
    }
    TimeArrayTimeZoneRule *tatzr = (TimeArrayTimeZoneRule*)&that;
    if (fTimeRuleType != tatzr->fTimeRuleType ||
        fNumStartTimes != tatzr->fNumStartTimes) {
        return false;
    }
    // Compare start times
    bool res = true;
    for (int32_t i = 0; i < fNumStartTimes; i++) {
        if (fStartTimes[i] != tatzr->fStartTimes[i]) {
            res = false;
            break;
        }
    }
    return res;
}

bool
TimeArrayTimeZoneRule::operator!=(const TimeZoneRule& that) const {
    return !operator==(that);
}

DateTimeRule::TimeRuleType
TimeArrayTimeZoneRule::getTimeType() const {
    return fTimeRuleType;
}

UBool
TimeArrayTimeZoneRule::getStartTimeAt(int32_t index, UDate& result) const {
    if (index >= fNumStartTimes || index < 0) {
        return false;
    }
    result = fStartTimes[index];
    return true;
}

int32_t
TimeArrayTimeZoneRule::countStartTimes() const {
    return fNumStartTimes;
}

UBool
TimeArrayTimeZoneRule::isEquivalentTo(const TimeZoneRule& other) const {
    if (this == &other) {
        return true;
    }
    if (typeid(*this) != typeid(other) || TimeZoneRule::isEquivalentTo(other) == false) {
        return false;
    }
    TimeArrayTimeZoneRule* that = (TimeArrayTimeZoneRule*)&other;
    if (fTimeRuleType != that->fTimeRuleType ||
        fNumStartTimes != that->fNumStartTimes) {
        return false;
    }
    // Compare start times
    UBool res = true;
    for (int32_t i = 0; i < fNumStartTimes; i++) {
        if (fStartTimes[i] != that->fStartTimes[i]) {
            res = false;
            break;
        }
    }
    return res;
}

UBool
TimeArrayTimeZoneRule::getFirstStart(int32_t prevRawOffset,
                                             int32_t prevDSTSavings,
                                             UDate& result) const {
    if (fNumStartTimes <= 0 || fStartTimes == nullptr) {
        return false;
    }
    result = getUTC(fStartTimes[0], prevRawOffset, prevDSTSavings);
    return true;
}

UBool
TimeArrayTimeZoneRule::getFinalStart(int32_t prevRawOffset,
                                     int32_t prevDSTSavings,
                                     UDate& result) const {
    if (fNumStartTimes <= 0 || fStartTimes == nullptr) {
        return false;
    }
    result = getUTC(fStartTimes[fNumStartTimes - 1], prevRawOffset, prevDSTSavings);
    return true;
}

UBool
TimeArrayTimeZoneRule::getNextStart(UDate base,
                                    int32_t prevRawOffset,
                                    int32_t prevDSTSavings,
                                    UBool inclusive,
                                    UDate& result) const {
    int32_t i = fNumStartTimes - 1;
    for (; i >= 0; i--) {
        UDate time = getUTC(fStartTimes[i], prevRawOffset, prevDSTSavings);
        if (time < base || (!inclusive && time == base)) {
            break;
        }
        result = time;
    }
    if (i == fNumStartTimes - 1) {
        return false;
    }
    return true;
}

UBool
TimeArrayTimeZoneRule::getPreviousStart(UDate base,
                                        int32_t prevRawOffset,
                                        int32_t prevDSTSavings,
                                        UBool inclusive,
                                        UDate& result) const {
    int32_t i = fNumStartTimes - 1;
    for (; i >= 0; i--) {
        UDate time = getUTC(fStartTimes[i], prevRawOffset, prevDSTSavings);
        if (time < base || (inclusive && time == base)) {
            result = time;
            return true;
        }
    }
    return false;
}


// ---- private methods ------

UBool
TimeArrayTimeZoneRule::initStartTimes(const UDate source[], int32_t size, UErrorCode& status) {
    // Free old array
    if (fStartTimes != nullptr && fStartTimes != fLocalStartTimes) {
        uprv_free(fStartTimes);
    }
    // Allocate new one if needed
    if (size > TIMEARRAY_STACK_BUFFER_SIZE) {
        fStartTimes = (UDate*)uprv_malloc(sizeof(UDate)*size);
        if (fStartTimes == nullptr) {
            status = U_MEMORY_ALLOCATION_ERROR;
            fNumStartTimes = 0;
            return false;
        }
    } else {
        fStartTimes = (UDate*)fLocalStartTimes;
    }
    uprv_memcpy(fStartTimes, source, sizeof(UDate)*size);
    fNumStartTimes = size;
    // Sort dates
    uprv_sortArray(fStartTimes, fNumStartTimes, (int32_t)sizeof(UDate), compareDates, nullptr, true, &status);
    if (U_FAILURE(status)) {
        if (fStartTimes != nullptr && fStartTimes != fLocalStartTimes) {
            uprv_free(fStartTimes);
        }
        fNumStartTimes = 0;
        return false;
    }
    return true;
}

UDate
TimeArrayTimeZoneRule::getUTC(UDate time, int32_t raw, int32_t dst) const {
    if (fTimeRuleType != DateTimeRule::UTC_TIME) {
        time -= raw;
    }
    if (fTimeRuleType == DateTimeRule::WALL_TIME) {
        time -= dst;
    }
    return time;
}

U_NAMESPACE_END

#endif /* #if !UCONFIG_NO_FORMATTING */

//eof

