#include <yandex/maps/wiki/common/date_time.h>
#include <maps/libs/common/include/exception.h>
#include <maps/libs/introspection/include/hashing.h>

#include <algorithm>
#include <array>
#include <cctype>
#include <ctime>
#include <iomanip>
#include <string>
#include <tuple>
#include <unordered_map>

#include <time.h>

namespace maps::wiki::common {

namespace {

constexpr int SECONDS_PER_DAY = SECONDS_PER_MINUTE * MINUTES_PER_HOUR * HOURS_PER_DAY;
constexpr int TM_YEAR_OFFSET = 1900;

const int SERVER_TIME_ZONE_OFFSET_SECONDS = []()
{
    auto nowTimestampUTC = std::time(nullptr);
    std::tm nowTm{};
    ASSERT(gmtime_r(&nowTimestampUTC, &nowTm) != nullptr); // treat nowTm as UTC time
    auto nowTimestampServer = std::mktime(&nowTm); // treat nowTm as server local time
    ASSERT(nowTimestampServer != -1);
    return nowTimestampServer - nowTimestampUTC;
}();

struct MonthDay
{
    Month month;
    Day day;

    auto introspect() const
    { return std::tie(month, day); }
};

using introspection::operator==;
using MonthDays = std::unordered_map<MonthDay, int, introspection::Hasher>;

const std::array<MonthDay, MAX_DAYS_PER_YEAR> DAY_OF_YEAR_TO_MONTH_DAY = []()
{
    std::array<MonthDay, MAX_DAYS_PER_YEAR> result;
    int month = 1;
    int day = 1;
    for (int i = 0; i < MAX_DAYS_PER_YEAR; ++i) {
        result[i] = MonthDay{Month(month), Day(day)};
        if (day < MAX_DAY_BY_MONTH[month - 1]) {
            ++day;
        } else {
            ++month;
            day = 1;
        }
    }
    return result;
}();

const MonthDays MONTH_DAY_TO_DAY_OF_YEAR = []()
{
    MonthDays result;
    result.reserve(MAX_DAYS_PER_YEAR);
    for (int i = 0; i < MAX_DAYS_PER_YEAR; ++i) {
        result[DAY_OF_YEAR_TO_MONTH_DAY[i]] = i + 1;
    }
    return result;
}();

// Splits 'abcd' digit string into 2 integer values, corresponding to 'ab' & 'cd'
std::pair<int, int>
extractTemporal(const std::string& str)
{
    if (str.size() != 4 ||
        !std::all_of(std::begin(str), std::end(str), isdigit)) {
            return {-1, -1};
    }

    return {std::stoi(str.substr(0, 2)),
            std::stoi(str.substr(2, 2))};
}

} // namespace

Date::Date(const Year& year, const Month& month, const Day& day)
    : year_(year)
    , month_(month)
    , day_(day)
{
}

Date::Date(const std::string& str)
{
    auto pair = extractTemporal(str);
    year_ = YEAR_ANY;
    month_ = Month(pair.first);
    day_ = Day(pair.second);
}

Date::Date(int dayOfYear)
    : Date(dayOfYear, YEAR_ANY)
{
}

Date::Date(int dayOfYear, const Year& year)
{
    REQUIRE(dayOfYear > 0 && dayOfYear <= MAX_DAYS_PER_YEAR,
        "Invalid day of year: " << dayOfYear);

    const auto& monthDay = DAY_OF_YEAR_TO_MONTH_DAY[dayOfYear - 1];
    year_ = year;
    month_ = monthDay.month;
    day_ = monthDay.day;
}

Date::Date(const std::string& str, const Date& defaultValue)
    : Date(str)
{
    if (!isValid()) {
        year_ = defaultValue.year_;
        month_ = defaultValue.month_;
        day_ = defaultValue.day_;
    }
}

bool
Date::isValid() const
{
    auto intMonth = static_cast<int>(month_);
    auto intDay = static_cast<int>(day_);
    return
        intMonth >= 1 &&
        intMonth <= MONTHS_PER_YEAR &&
        intDay >=1 &&
        intDay <= MAX_DAY_BY_MONTH[intMonth - 1];
}

WeekdayFlags
Date::weekday() const
{
    auto timestamp = DateTime(*this, DAY_START_TIME).timestamp();
    std::tm tm{};
    ASSERT(gmtime_r(&timestamp, &tm) != nullptr);
    return static_cast<WeekdayFlags>(
        1 << ((tm.tm_wday - 1 + DAYS_PER_WEEK) % DAYS_PER_WEEK));
}

int
Date::dayOfYear() const
{
    if (!isValid()) {
        return -1;
    }
    return MONTH_DAY_TO_DAY_OF_YEAR.at({month_, day_});
}

Date
Date::next() const
{
    return addDays(1);
}

Date
Date::addDays(int days) const
{
    REQUIRE(std::abs(days) < MAX_DAYS_PER_YEAR,
        "Invalid number of days to add: " << days);
    auto newDayOfYear = dayOfYear() + days;
    auto newYear = year_;
    if (newDayOfYear <= 0) {
        newDayOfYear += MAX_DAYS_PER_YEAR;
        if (newYear != YEAR_ANY) {
            newYear -= static_cast<Year>(1);
        }
    }
    if (newDayOfYear > MAX_DAYS_PER_YEAR) {
        newDayOfYear -= MAX_DAYS_PER_YEAR;
        if (newYear != YEAR_ANY) {
            newYear += static_cast<Year>(1);
        }
    }
    return Date(newDayOfYear, newYear);
}

bool
Date::operator==(const Date& date) const
{
    if (year_ == YEAR_ANY || date.year_ == YEAR_ANY) {
        return std::tie(month_, day_) ==
               std::tie(date.month_, date.day_);
    }
    return std::tie(year_, month_, day_) ==
           std::tie(date.year_, date.month_, date.day_);
}

Time::Time(const Hour& hour, const Minute& minute)
    : hour_(hour)
    , minute_(minute)
{
}

Time::Time(const std::string& str)
{
    auto pair = extractTemporal(str);
    hour_ = Hour(pair.first);
    minute_ = Minute(pair.second);
}

bool
Time::isValid() const
{
    auto intHour = static_cast<int>(hour_);
    auto intMinute = static_cast<int>(minute_);
    return
        // From 00:00 till 47:59 is valid because of "25th hour" feature
        intHour >= 0 && intHour <= 47 &&
        intMinute >= 0 && intMinute <= 59;
}

DateTime::DateTime(const Date& date, const Time& time)
    : date_(date)
    , time_(time)
{
}

DateTime::DateTime(std::time_t timestamp)
    : date_(YEAR_START_DATE)
    , time_(DAY_START_TIME)
{
    std::tm tm{};
    ASSERT(gmtime_r(&timestamp, &tm) != nullptr);
    date_ = Date(
        Year(tm.tm_year + TM_YEAR_OFFSET),
        Month(tm.tm_mon + 1),
        Day(tm.tm_mday));
    time_ = Time(Hour(tm.tm_hour), Minute(tm.tm_min));
}

std::time_t
DateTime::timestamp() const
{
    REQUIRE(date_.year() != YEAR_ANY,
        "Converting date without year to timestamp is not supported");

    std::tm tm{};
    tm.tm_year = static_cast<int>(date_.year() - TM_YEAR_OFFSET);
    tm.tm_mon = static_cast<int>(date_.month() - 1);
    tm.tm_mday = static_cast<int>(date_.day());
    tm.tm_hour = static_cast<int>(time_.hour());
    tm.tm_min = static_cast<int>(time_.minute());
    tm.tm_sec = 0;
    tm.tm_isdst = -1;

    auto timestampWithServerTZShift = std::mktime(&tm);
    ASSERT(timestampWithServerTZShift != -1);
    return timestampWithServerTZShift - SERVER_TIME_ZONE_OFFSET_SECONDS;
}

DateTime
DateTime::now()
{
    return DateTime(std::time(nullptr));
}

std::ostream&
operator<<(std::ostream& os, const Date& date)
{
    os << std::setfill('0') << std::setw(2) << date.day()
       << "."
       << std::setfill('0') << std::setw(2) << date.month();
    if (date.year() != YEAR_ANY) {
       os << "."
          << std::setfill('0') << std::setw(4) << date.year();
    }
    return os;
}

std::ostream&
operator<<(std::ostream& os, const Time& time)
{
    os << std::setfill('0') << std::setw(2) << time.hour()
       << ":"
       << std::setfill('0') << std::setw(2) << time.minute()
       << ":"
       << "00"; // seconds
    return os;
}

int
differenceDays(const Date& lhs, const Date& rhs)
{
    auto leftTimestamp = DateTime(lhs, DAY_START_TIME).timestamp();
    auto rightTimestamp = DateTime(rhs, DAY_START_TIME).timestamp();
    auto diffSec = leftTimestamp - rightTimestamp;
    return diffSec / SECONDS_PER_DAY;
}

std::string
canonicalDateTimeString(const std::string& str, WithTimeZone withTimeZone)
{
    constexpr char STR_POSITIVE_TIME_ZONE = '+';
    constexpr char STR_NEGATIVE_TIME_ZONE = '-';
    constexpr char STR_TIME_FRACTION_DELIMITER = ':';
    constexpr char SQL_SECONDS_DELIMITER = '.';
    constexpr char SQL_DATETIME_DELIMITER = ' ';
    constexpr char ISO_DATETIME_DELIMITER = 'T';
    static const std::string STR_TIME_ZERO_FRACTION = "00";

    size_t timeZonePos = str.rfind(STR_POSITIVE_TIME_ZONE);
    if (timeZonePos == std::string::npos) {
        size_t negativeTimeZonePos = str.rfind(STR_NEGATIVE_TIME_ZONE);
        size_t timeFractionPos = str.find(STR_TIME_FRACTION_DELIMITER);
        if (negativeTimeZonePos != std::string::npos && timeFractionPos != std::string::npos &&
            negativeTimeZonePos > timeFractionPos) {
            timeZonePos = negativeTimeZonePos;
        }
    }
    std::string timeZoneStr;
    if (std::string::npos != timeZonePos && withTimeZone == WithTimeZone::Yes) {
        timeZoneStr = str.substr(timeZonePos);
        if (std::string::npos == timeZoneStr.find(STR_TIME_FRACTION_DELIMITER)) {
            timeZoneStr += STR_TIME_FRACTION_DELIMITER + STR_TIME_ZERO_FRACTION;
        }
    }
    auto dateTimeStr = str.substr(0, str.find(SQL_SECONDS_DELIMITER));
    dateTimeStr = dateTimeStr.substr(0, timeZonePos);
    std::replace(
        dateTimeStr.begin(), dateTimeStr.end(),
        SQL_DATETIME_DELIMITER,
        ISO_DATETIME_DELIMITER);

    return WithTimeZone::No == withTimeZone
        ? dateTimeStr
        : dateTimeStr + timeZoneStr;
}

} // namespace maps::wiki::common
