#include <maps/wikimap/mapspro/services/mrc/libs/common/include/suncalc.h>

#include <cmath>

namespace maps {
namespace mrc {
namespace common {

namespace {

// Official Sun's zenith at sunrise/sunset
constexpr double ZENITH_COS = -0.014486;

inline double toRadians(double deg) { return deg * M_PI / 180.0; }

inline double fromRadians(double rad) { return rad * 180.0 / M_PI; }

inline double sind(double deg) { return sin(toRadians(deg)); }

inline double cosd(double deg) { return cos(toRadians(deg)); }

inline double tand(double deg) { return tan(toRadians(deg)); }

int dayOfYear(int day, int month, int year)
{
    int n1 = floor(275 * month / 9);
    int n2 = floor((month + 9) / 12);
    int n3 = (1 + floor((year - 4 * floor(year / 4) + 2) / 3));
    return n1 - (n2 * n3) + day - 30;
}

} // anonymous namespace


// Source: http://williams.best.vwh.net/sunrise_sunset_example.htm
std::pair<double, double>
getSunriseSunsetHours(int day, int month, int year, double lat, double lon)
{
    double doy = dayOfYear(day, month, year);
    double lonHour = lon / 15;

    // In all arrays below index 0 is for sunrise and index 1 is for sunset
    // Using original paper's variable names for convenience.
    double t[2];          // Approximate sunrise/sunset time
    double M[2];          // Sun's mean anomaly
    double L[2];          // Sun's true longitude
    double RA[2];         // Sun's right ascension
    double Lquadrant[2];  // Sun's true longitude quadrant
    double RAquadrant[2]; // Sun's right ascension quadrant
    double sinDec[2];     // Sun's declination sin
    double cosDec[2];     // Sun's declination cos
    double cosH[2];       // Sun's local hour angle cos

    t[0] = doy + ((6.0 - lonHour) / 24);
    t[1] = doy + ((18.0 - lonHour) / 24);

    for (int i = 0; i < 2; ++i) {
        M[i] = (0.9856 * t[i]) - 3.289;
        L[i] = M[i] + (1.916 * sind(M[i])) + (0.02 * sind(2 * M[i])) + 282.634;
        L[i] = fmod(L[i], 360.0);
        RA[i] = fromRadians(atan(0.91764 * tand(L[i])));
        RA[i] = fmod(RA[i], 360.0);

        // RA value needs to be in the same quadrant as L
        Lquadrant[i] = floor(L[i] / 90) * 90;
        RAquadrant[i] = floor(RA[i] / 90) * 90;
        RA[i] += (Lquadrant[i] - RAquadrant[i]);
        RA[i] /= 15;

        sinDec[i] = 0.39782 * sind(L[i]);
        cosDec[i] = cos(asin(sinDec[i]));
        cosH[i] = (ZENITH_COS - (sinDec[i] * sind(lat)))
                  / (cosDec[i] * cosd(lat));
    }

    if (cosH[0] > 1) {
        return {NO_SUNRISE, NO_SUNRISE};
    }
    if (cosH[1] < -1) {
        return {NO_SUNSET, NO_SUNSET};
    }

    double H[2];  // Sun's local hour
    double T[2];  // Mean time of rising/setting
    double UT[2]; // Time of rising/setting in UTC

    H[0] = (360.0 - fromRadians(acos(cosH[0]))) / 15;
    H[1] = (fromRadians(acos(cosH[1]))) / 15;
    for (int i = 0; i < 2; ++i) {
        T[i] = H[i] + RA[i] - (0.06571 * t[i]) - 6.622;
        UT[i] = fmod(fmod(T[i] - lonHour, 24.0) + 24.0, 24.0);
    }

    return {UT[0], UT[1]};
}


DayPart getDayPart(chrono::TimePoint timestamp, double lat, double lon)
{
    time_t time = chrono::convertToUnixTime(timestamp);
    tm utcTime;
    utcTime = *gmtime_r(&time, &utcTime);

    auto day = utcTime.tm_mday;         // tm_mday is one-based
    auto month = utcTime.tm_mon + 1;    // tm_mon is zero-based
    auto year = utcTime.tm_year + 1900; // tm_year is year since 1900
    double hrs = static_cast<double>(utcTime.tm_min) / 60. + utcTime.tm_hour;

    double sunrise, sunset;
    std::tie(sunrise, sunset)
        = getSunriseSunsetHours(day, month, year, lat, lon);

    if (sunrise == NO_SUNRISE) {
        return DayPart::Night;
    } else if (sunset == NO_SUNSET) {
        return DayPart::Day;
    } else if (sunrise < sunset) {
        return hrs > sunrise && hrs < sunset ? DayPart::Day : DayPart::Night;
    } else {
        return hrs > sunrise || hrs < sunset ? DayPart::Day : DayPart::Night;
    }
}

} // common
} // mrc
} // maps
