/********************************************************
 *  ██████╗  ██████╗████████╗██╗
 * ██╔════╝ ██╔════╝╚══██╔══╝██║
 * ██║  ███╗██║        ██║   ██║
 * ██║   ██║██║        ██║   ██║
 * ╚██████╔╝╚██████╗   ██║   ███████╗
 *  ╚═════╝  ╚═════╝   ╚═╝   ╚══════╝
 * Geophysical Computational Tools & Library (GCTL)
 *
 * Copyright (c) 2022  Yi Zhang (yizhang-geo@zju.edu.cn)
 *
 * GCTL is distributed under a dual licensing scheme. You can redistribute 
 * it and/or modify it under the terms of the GNU Lesser General Public 
 * License as published by the Free Software Foundation, either version 2 
 * of the License, or (at your option) any later version. You should have 
 * received a copy of the GNU Lesser General Public License along with this 
 * program. If not, see <http://www.gnu.org/licenses/>.
 * 
 * If the terms and conditions of the LGPL v.2. would prevent you from using 
 * the GCTL, please consider the option to obtain a commercial license for a 
 * fee. These licenses are offered by the GCTL's original author. As a rule, 
 * licenses are provided "as-is", unlimited in time for a one time fee. Please 
 * send corresponding requests to: yizhang-geo@zju.edu.cn. Please do not forget 
 * to include some description of your company and the realm of its activities. 
 * Also add information on how to contact you by electronic and paper mail.
 ******************************************************/

#include "utc_time.h"

const int DaysInMonth[13] = {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
const std::string MonthName[13] = {"Null", "Jan.", "Feb.", "Mar.", "Apr."," May", "Jun.", "Jul.", "Aug.", "Sep.", "Oct.", "Nov.", "Dec."};

std::string gctl::time_stamp()
{
    time_t now = time(0);
	char* dt = ctime(&now);
    return std::string(dt);
}

// Function returns true if the given year is a leap year and returns false otherwise.
bool gctl::is_leap_year(int year)
{
    if (year < 1)
    {
        throw std::invalid_argument("[GCTL_SEIMIC] Invalid date input for gctl::is_leap_year(...)");
    }

    return year % 4 == 0 && ( year % 100 != 0 || year % 400 == 0 );
}

// Compute the month and day corresponding to the Julian day within the given year.
void gctl::month_and_day(int year, int julian_day, int &month, int &day )
{
    if (year < 1 || julian_day > 366)
    {
        throw std::invalid_argument("[GCTL_SEIMIC] Invalid date input for gctl::month_and_day(...)");
    }

    if (!is_leap_year(year) && julian_day > 365)
    {
        throw std::invalid_argument("[GCTL_SEIMIC] Invalid date input for gctl::month_and_day(...)");
    }

    bool month_found = false;
    month = 1;

    // Loop through the months, subtracting the days in this month 
    // from the Julian day, until the month is found where the 
    // remaining days is less than or equal to the total days in the month.
    while (!month_found)
    {
        // Calculate the days in this month by looking it up in the array. Add one if it is a leap year.
        int days_this_month = DaysInMonth[month];
        if (month == 2 && is_leap_year(year)) ++days_this_month;

        if (julian_day <= days_this_month) month_found = true; // Done!
        else 
        {
            julian_day -= days_this_month;
            ++ month;
        }
    }

    day = julian_day;
    return;
}

// Compute the Julian day
void gctl::julian_day(int year, int month, int day, int &julian_day)
{
    if (year < 1 || month < 1 || month > 12 || day < 1)
    {
        throw std::invalid_argument("[GCTL_SEIMIC] Invalid date input for gctl::julian_day(...)");
    }

    if (day > DaysInMonth[month] && !(month == 2 && is_leap_year(year) && day < 30))
    {
        throw std::invalid_argument("[GCTL_SEIMIC] Invalid date input for gctl::julian_day(...)");
    }
    
    julian_day = day;
    for (size_t i = 0; i < month; i++)
    {
        julian_day += DaysInMonth[i];
    }

    if (month > 2 && is_leap_year(year))
    {
        julian_day++;
    }
    return;
}

void gctl::decimal_year2date(double deci_year, int &year, int &month, int &day)
{
    year = floor(deci_year);

    int julian_day;
    if (is_leap_year(year))
    {
        julian_day = round(366.0*(deci_year - year));
        julian_day = std::max(std::min(julian_day, 366), 1);
    }
    else
    {
        julian_day = round(365.0*(deci_year - year));
        julian_day = std::max(std::min(julian_day, 365), 1);
    }

    month_and_day(year, julian_day, month, day);
    return;
}

gctl::UTC_TIME::UTC_TIME()
{
    year = 1900; month = 1; day = 1; julday = 1;
    hour = 0; mint = 0; sec = 0; msec = 0;
}

gctl::UTC_TIME::UTC_TIME(int y, int m, int d, int h, int mt, int s, int ms)
{
    set_time(y, m, d, h, mt, s, ms);
}

gctl::UTC_TIME::UTC_TIME(int y, int jd, int h, int mt, int s, int ms)
{
    set_time(y, jd, h, mt, s, ms);
}

gctl::UTC_TIME::UTC_TIME(std::string t_str)
{
    set_time(t_str);
}

void gctl::UTC_TIME::set_time(int y, int m, int d, int h, int mt, int s, int ms)
{
    if (h < 0 || h > 23 || mt < 0 || mt > 59 || s < 0 || s > 59 || ms < 0 || ms > 999)
    {
        throw std::invalid_argument("[GCTL_SEISMIC] Invalid time parameters for gctl::UTC_TIME::set_time(...)");
    }

    if (y < 1900 || m < 1 || m > 12 || d < 1 || d > 31)
    {
        throw std::invalid_argument("[GCTL_SEISMIC] Invalid date parameters for gctl::UTC_TIME::set_time(...)");
    }

    if (d > DaysInMonth[m])
    {
        if (is_leap_year(y) && m == 2 && d == 29){}
        else throw std::invalid_argument("[GCTL_SEISMIC] Invalid date parameters for gctl::UTC_TIME::set_time(...)");
    }

    //h %= 24; mt %= 60; s %= 60; ms %= 1000;
    year = y; month = m; day = d; hour = h;
    mint = mt; sec = s; msec = ms;
    julian_day(year, month, day, julday);
    return;
}

void gctl::UTC_TIME::set_time(int y, int jd, int h, int mt, int s, int ms)
{
    if (h < 0 || h > 23 || mt < 0 || mt > 59 || s < 0 || s > 59 || ms < 0 || ms > 999)
    {
        throw std::invalid_argument("[GCTL_SEISMIC] Invalid time parameters for gctl::UTC_TIME::set_time(...)");
    }

    if (y < 1900)
    {
        throw std::invalid_argument("[GCTL_SEISMIC] Invalid date parameters for gctl::UTC_TIME::set_time(...)");
    }

    if (jd > 365)
    {
        if (is_leap_year(y) && jd == 366){}
        else throw std::invalid_argument("[GCTL_SEISMIC] Invalid date parameters for gctl::UTC_TIME::set_time(...)");
    }

    //h %= 24; mt %= 60; s %= 60; ms %= 1000;
    year = y; julday = jd; hour = h;
    mint = mt; sec = s; msec = ms;
    month_and_day(year, julday, month, day);
    return;
}

void gctl::UTC_TIME::set_time(std::string t_str)
{   
    int y, m, d, h, mt, s, ms;
    if (7 != sscanf(t_str.c_str(), "%d-%d-%dT%d:%d:%d.%d", &y, &m, &d, &h, &mt, &s, &ms))
    {
        throw std::invalid_argument("[GCTL_SEISMIC] Invalid time string for gctl::UTC_TIME::set_time(...)");
    }

    if (h < 0 || h > 23 || mt < 0 || mt > 59 || s < 0 || s > 59 || ms < 0 || ms > 999)
    {
        throw std::invalid_argument("[GCTL_SEISMIC] Invalid time parameters for gctl::UTC_TIME::set_time(...)");
    }

    if (y < 1900 || m < 1 || m > 12 || d < 1 || d > 31)
    {
        throw std::invalid_argument("[GCTL_SEISMIC] Invalid date parameters for gctl::UTC_TIME::set_time(...)");
    }

    if (d > DaysInMonth[m])
    {
        if (is_leap_year(y) && m == 2 && d == 29){}
        else throw std::invalid_argument("[GCTL_SEISMIC] Invalid date parameters for gctl::UTC_TIME::set_time(...)");
    }

    year = y; month = m; day = d; hour = h;
    mint = mt; sec = s; msec = ms;
    julian_day(year, month, day, julday);
    return;
}

void gctl::UTC_TIME::set_rdseed_time(std::string t_str)
{
    sscanf(t_str.c_str(), "%d,%d,%d:%d:%d.%d", &year, &julday, &hour, &mint, &sec, &msec);

    msec = round((double) msec/10);
    month_and_day(year, julday, month, day);
    return;
}

void gctl::UTC_TIME::set_to_now()
{
    struct timeval tv;
    //struct timezone tz;
 
    //gettimeofday(&tv, &tz);
    gettimeofday(&tv, nullptr);
    std::tm cur_t = *gmtime(&tv.tv_sec);

    year = cur_t.tm_year + 1900;
    month = cur_t.tm_mon + 1;
    day = cur_t.tm_mday;
    hour = cur_t.tm_hour;
    mint = cur_t.tm_min;
    sec = cur_t.tm_sec;
    msec = tv.tv_usec/1000;
    julian_day(year, month, day, julday);
    return;
}

void gctl::UTC_TIME::add_duration(double s)
{
    if (s < 0)
    {
        throw std::invalid_argument("[GCTL_SEIMIC] Invalid time duration for gctl::UTC_TIME::add_duration(...)");
    }
    
    msec += round(1000*(s - floor(s))); sec += msec/1000; msec %= 1000;

    int t = floor(s);
    sec += t%60;  mint +=   sec/60;  sec %= 60; t = t/60;
    mint += t%60; hour +=   mint/60; mint %= 60; t = t/60;
    hour += t%24; julday += hour/24; hour %= 24; t = t/24;

    julday += t;
    while (julday > 366)
    {
        if (!is_leap_year(year) && julday == 365) break;

        if (is_leap_year(year)) {julday -= 366; year++;}
        else {julday -= 365; year++;}
    }

    month_and_day(year, julday, month, day);
    return;
}

void gctl::UTC_TIME::round_up_to(time_unit tu)
{
    if (tu == Second)
    {
        if (msec != 0)
        {
            msec = 0;
            add_duration(1);
        }
        return;
    }
    else if (tu == Mintue)
    {
        if (msec != 0 || sec != 0)
        {
            msec = 0;
            add_duration(60 - sec);
        }
        return;
    }
    else if (tu == Hour)
    {
        if (msec != 0 || sec != 0 || mint != 0)
        {
            sec = 0; msec = 0;
            add_duration((60 - mint)*60);
        }
        return;
    }
    else if (tu == Day)
    {
        if (msec != 0 || sec != 0 || mint != 0 || hour != 0)
        {
            mint = 0; sec = 0; msec = 0;
            add_duration((24 - hour)*3600);
        }
        return;
    }
    else if (tu == Month || tu == Year)
    {
        throw std::invalid_argument("[GCTL_SEISMIC] Invalid parameter for gctl::UTC_TIME::round_up_to(...)");
    }
    else return;
}

double gctl::UTC_TIME::diff_sec(const UTC_TIME &t) const
{
    double d = sec - t.sec 
        + 0.001*(msec - t.msec) 
        + 60*(mint - t.mint) 
        + 3600*(hour - t.hour) 
        + 24*3600*(julday - t.julday) 
        + 365*24*3600*(year - t.year);

    if (year > t.year)
    {
        for (int i = t.year; i < year; i++)
        {
            if (is_leap_year(i)) d += 24*3600;
        }
    }

    if (year < t.year)
    {
        for (int i = year; i < t.year; i++)
        {
            if (is_leap_year(i)) d -= 24*3600;
        }
    }
    return d;
}

std::string gctl::UTC_TIME::time_str(bool symbolic) const
{
    double f_sec = 1.0*sec + 0.001*msec;

    std::stringstream ss;
    ss << std::fixed << std::setprecision(3) << f_sec;
    std::string s_sec = ss.str();

    if (symbolic) return MonthName[month] + " " + std::to_string(day) + " " + std::to_string(year) + " " + std::to_string(hour)+":"+std::to_string(mint)+":"+ s_sec;
    return std::to_string(year)+"-"+std::to_string(month)+"-"+std::to_string(day)+"T"+std::to_string(hour)+":"+std::to_string(mint)+":"+s_sec;
}

std::string gctl::UTC_TIME::rdseed_time_str() const
{
    double f_sec = 1.0*sec + 0.001*msec;

    std::stringstream ss;
    ss << std::fixed << std::setprecision(4) << f_sec;
    std::string s_str = ss.str();
    if (sec < 10) s_str = "0" + s_str;

    std::string jday_str = std::to_string(julday);
    if (julday < 10) jday_str = "00" + jday_str;
    else if (julday < 100) jday_str = "0" + jday_str;

    std::string h_str = std::to_string(hour);
    if (hour < 10) h_str = "0" + h_str;

    std::string m_str = std::to_string(mint);
    if (mint < 10) m_str = "0" + m_str;

    return std::to_string(year)+"."+jday_str+"."+h_str+"."+m_str+"."+s_str;
}

bool gctl::operator==(const gctl::UTC_TIME &ta, const gctl::UTC_TIME &tb)
{
    if (ta.year != tb.year || ta.julday != tb.julday || ta.hour != tb.hour || 
        ta.mint != tb.mint || ta.sec != tb.sec || ta.msec != tb.msec)
    {
        return false;
    }
    return true;
}

bool gctl::operator!=(const gctl::UTC_TIME &ta, const gctl::UTC_TIME &tb)
{
    if (ta.year != tb.year || ta.julday != tb.julday || ta.hour != tb.hour || 
        ta.mint != tb.mint || ta.sec != tb.sec || ta.msec != tb.msec)
    {
        return true;
    }
    return false;
}

bool gctl::operator<(const gctl::UTC_TIME &ta, const gctl::UTC_TIME &tb)
{
    if (tb.year < ta.year) return false;
    else if (tb.year > ta.year) return true;

    if (tb.julday < ta.julday) return false;
    else if (tb.julday > ta.julday) return true;

    if (tb.hour < ta.hour) return false;
    else if (tb.hour > ta.hour) return true;

    if (tb.mint < ta.mint) return false;
    else if (tb.mint > ta.mint) return true;

    if (tb.sec < ta.sec) return false;
    else if (tb.sec > ta.sec) return true;

    if (tb.msec <= ta.msec) return false;
    else return true;
}

bool gctl::operator<=(const gctl::UTC_TIME &ta, const gctl::UTC_TIME &tb)
{
    if (tb.year < ta.year) return false;
    else if (tb.year > ta.year) return true;

    if (tb.julday < ta.julday) return false;
    else if (tb.julday > ta.julday) return true;

    if (tb.hour < ta.hour) return false;
    else if (tb.hour > ta.hour) return true;

    if (tb.mint < ta.mint) return false;
    else if (tb.mint > ta.mint) return true;

    if (tb.sec < ta.sec) return false;
    else if (tb.sec > ta.sec) return true;

    if (tb.msec < ta.msec) return false;
    else return true;
}