/*****************************************************************************************
 *                                                                                       *
 * OpenSpace                                                                             *
 *                                                                                       *
 * Copyright (c) 2014-2020                                                               *
 *                                                                                       *
 * Permission is hereby granted, free of charge, to any person obtaining a copy of this  *
 * software and associated documentation files (the "Software"), to deal in the Software *
 * without restriction, including without limitation the rights to use, copy, modify,    *
 * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to    *
 * permit persons to whom the Software is furnished to do so, subject to the following   *
 * conditions:                                                                           *
 *                                                                                       *
 * The above copyright notice and this permission notice shall be included in all copies *
 * or substantial portions of the Software.                                              *
 *                                                                                       *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,   *
 * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A         *
 * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT    *
 * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF  *
 * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE  *
 * OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                                         *
 ****************************************************************************************/

#include <modules/globebrowsing/src/timequantizer.h>

#include <openspace/util/spicemanager.h>
#include <openspace/util/time.h>
#include <ghoul/fmt.h>
#include <ghoul/glm.h>
#include <ghoul/misc/assert.h>
#include <ghoul/misc/exception.h>
#include <ghoul/misc/profiling.h>
#include <date/date.h>
#include <algorithm>
#include <charconv>
#include <iomanip>
#include <sstream>

// @TODO (abock, 2020-08-07) All of the time handling in this class should be cleaned up
//       a bit. There are lots of conversions between ISO strings for time and Time
//       objects and back which eat up performance.  For example, the TimeRange should
//       operate on Time objects rather than date strings and the DateTime likewise (if
//       this class needs to exist at all)

namespace openspace::globebrowsing {

namespace {
    // returns the number of days in a given month and year (takes leap year into account)
    constexpr int monthSize(int month, int year) {
        date::year_month_day_last d = date::year(year) / date::month(month) / date::last;
        return static_cast<int>(static_cast<unsigned>(d.day()));
    }

    /**
     * singleIncrement is used for any of the date/time types, and handles overflow
     * values using the min/max parameters
     *
     * \param oper the date/time variable to operate on (will be changed)
     * \param val the value of the increment, which may be changed in this function
     *        if an overflow occurs
     * \param min the minimum allowable value
     * \param max the maximum allowable value (determines where overflow occurs)
     */
    bool singleIncrement(int& oper, int& val, int min, int max) {
        oper += val;
        if (oper <= max) {
            return true;
        }
        oper = oper - max - (1 - min);
        // Only single increments for the less-significant units on rollover
        val = 1;
        return false;
    }

    /**
     * singleDecrement is used for any of the date/time types, and handles underflow
     * values using the min/max parameters
     *
     * \param oper the date/time variable to operate on (will be changed)
     * \param val the value of the decrement, which may be changed in this function
     *        if an underflow occurs
     * \param min the minimum allowable value
     * \param max the maximum allowable value (determines where underflow occurs)
     */
    bool singleDecrement(int& oper, int& val, int min, int max) {
        oper -= val;
        if (oper >= min) {
            return true;
        }
        oper = oper + max + (1 - min);
        // Only single increments for the less-significant units on rollover
        val = 1;
        return false;
    }

} // namespace

RangedTime::RangedTime(std::string start, std::string end)
    : _start(std::move(start))
    , _end(std::move(end))
{
    setStart(_start);
    setEnd(_end);
}

void RangedTime::setStart(std::string start) {
    Time t1;
    t1.setTime(start);
    _startJ2000 = t1.j2000Seconds();
    _start = std::move(start);
}

void RangedTime::setEnd(std::string end) {
    Time t2;
    t2.setTime(end);
    _endJ2000 = t2.j2000Seconds();
    _end = std::move(end);
}

bool RangedTime::includes(const Time& checkTime) const {
    const double tj = checkTime.j2000Seconds();
    return (_startJ2000 <= tj && tj <= _endJ2000);
}

const char* RangedTime::clamp(const char* checkTime) {
    Time t;
    t.setTime(checkTime);
    const double tj = t.j2000Seconds();
    if (tj < _startJ2000) {
        return _start.c_str();
    }
    else if (tj > _endJ2000) {
        return _end.c_str();
    }
    else {
        return checkTime;
    }
}

std::string_view RangedTime::start() const {
    return _start;
}

std::string_view RangedTime::end() const {
    return _end;
}

DateTime::DateTime(std::string_view initDateTime) {
    setTime(initDateTime);
};

void DateTime::setTime(std::string_view input) {
    // Indices into an ISO8601 YYYY-MM-ddTHH:mm:ss string
    constexpr const size_t IndexYear = 0;
    constexpr const size_t IndexMonth = 5;
    constexpr const size_t IndexDay = 8;
    constexpr const size_t IndexHour = 11;
    constexpr const size_t IndexMinute = 14;
    constexpr const size_t IndexSecond = 17;

    std::from_chars(input.data() + IndexYear, input.data() + IndexYear + 4, _year);
    std::from_chars(input.data() + IndexMonth, input.data() + IndexMonth + 2, _month);
    std::from_chars(input.data() + IndexDay, input.data() + IndexDay + 2, _day);
    std::from_chars(input.data() + IndexHour, input.data() + IndexHour + 2, _hour);
    std::from_chars(input.data() + IndexMinute, input.data() + IndexMinute + 2, _minute);
    std::from_chars(input.data() + IndexSecond, input.data() + IndexSecond + 2, _second);
}

std::string DateTime::ISO8601() const {
    return fmt::format(
        "{:0>4}-{:0>2}-{:0>2}T{:0>2}:{:0>2}:{:0>2}",
        _year, _month, _day, _hour, _minute, _second
    );
};

double DateTime::J2000() const {
    char Buffer[20];
    std::memset(Buffer, 0, 20);
    fmt::format_to(
        Buffer,
        "{:0>4}-{:0>2}-{:0>2}T{:0>2}:{:0>2}:{:0>2}",
        _year, _month, _day, _hour, _minute, _second
    );
    return Time::convertTime(Buffer);
}

void DateTime::operator=(DateTime& src) {
    _year = src.year();
    _month = src.month();
    _day = src.day();
    _hour = src.hour();
    _minute = src.minute();
    _second = src.second();
}

int DateTime::increment(int value, char unit, double error, double resolution) {
    unsigned int nIncrements = std::abs(static_cast<int>(error / resolution));
    if (nIncrements == 0) {
        nIncrements = 1;
    }
    for (unsigned int i = 0; i < nIncrements; ++i) {
        incrementOnce(value, unit);
    }
    return nIncrements;
}

void DateTime::incrementOnce(int value, char unit) {
    bool inBounds = true;
    switch (unit) {
        case 'm':
            if (singleIncrement(_minute, value, 0, 59)) {
                break;
            }
            [[ fallthrough ]];
        case 'h':
            if (singleIncrement(_hour, value, 0, 23)) {
                break;
            }
            [[ fallthrough ]];
        case 'd':
            if (singleIncrement(_day, value, 1, monthSize(_month, _year))) {
                break;
            }
            [[ fallthrough ]];
        case 'M':
            inBounds = singleIncrement(_month, value, 1, 12);
            _day = std::clamp(_day, 1, monthSize(_month, _year));
            if (inBounds) {
                break;
            }
            [[ fallthrough ]];
        case 'y':
            _year += value;
            break;
        default:
            throw ghoul::RuntimeError(fmt::format(
                "Invalid unit in incrementOnce '{}'. Expected 'y', 'M', 'd', 'h', or 'm'",
                unit
            ));
    }
}

int DateTime::decrement(int value, char unit, double error, double resolution) {
    unsigned int nDecrements = std::abs(static_cast<int>(error / resolution));
    if (nDecrements == 0) {
        nDecrements = 1;
    }
    for (unsigned int i = 0; i < nDecrements; ++i) {
        decrementOnce(value, unit);
    }
    return nDecrements;
}

void DateTime::decrementOnce(int value, char unit) {
    bool inBounds = true;
    switch (unit) {
        case 'm':
            if (singleDecrement(_minute, value, 0, 59)) {
                break;
            }
            // else fall-through if underflow...

        case 'h':
            if (singleDecrement(_hour, value, 0, 23)) {
                break;
            }
            // else fall-through if underflow...

        case 'd':
            if (singleDecrement(_day, value, 1,
                monthSize(_month == 1 ? 12 : _month - 1, _year)))
            {
                break;
            }
            // else fall-through if underflow...

        case 'M':
            inBounds = singleDecrement(_month, value, 1, 12);
            _day = std::clamp(_day, 1, monthSize(_month, _year));
            if (inBounds) {
                break;
            }
            // else fall-through if underflow...

        case 'y':
            _year -= value;
            break;

        default:
            throw ghoul::RuntimeError(
                "Invalid unit format in TQ decrementOnce '" + std::to_string(unit) +
                "'. Expected 'y', 'M', 'd', 'h', or 'm'."
            );
    }
}

int DateTime::year() const {
    return _year;
}

int DateTime::month() const {
    return _month;
}

int DateTime::day() const {
    return _day;
}

int DateTime::hour() const {
    return _hour;
}

int DateTime::minute() const {
    return _minute;
}

int DateTime::second() const {
    return _second;
}

void DateTime::setYear(int y) {
    _year = y;
}

void DateTime::setMonth(int m) {
    _month = m;
}

void DateTime::setDay(int d) {
    _day = d;
}

void DateTime::setHour(int h) {
    _hour = h;
}

void DateTime::setMinute(int m) {
    _minute = m;
}

void DateTime::setSecond(int s) {
    _second = (s > 0) ? ((s <= 59) ? s : 59) : 0;
}

TimeQuantizer::TimeQuantizer(std::string start, std::string end,
                             const std::string& resolution)
    : _start(start)
    , _timerange(std::move(start), std::move(end))
{
    verifyStartTimeRestrictions();
    _resolution = parseTimeResolutionStr(resolution);
}

double TimeQuantizer::parseTimeResolutionStr(const std::string& resolutionStr) {
    const char unit = resolutionStr.back();
    std::string numberString = resolutionStr.substr(0, resolutionStr.length() - 1);

    char* p;
    double value = strtol(numberString.c_str(), &p, 10);
    _resolutionValue = value;
    _resolutionUnit = unit;
    if (*p) { // not a number
        throw ghoul::RuntimeError("Cannot convert " + numberString + " to number");
    }
    else {
        verifyResolutionRestrictions(static_cast<int>(value), unit);
        return computeSecondsFromResolution(static_cast<int>(value), unit);
    }
}

void TimeQuantizer::setStartEndRange(const std::string& start, const std::string& end) {
    _start.setTime(start);
    verifyStartTimeRestrictions();

    _timerange.setStart(start);
    _timerange.setEnd(end);
}

void TimeQuantizer::setResolution(const std::string& resolutionString) {
    _resolution = parseTimeResolutionStr(resolutionString);
}

void TimeQuantizer::verifyStartTimeRestrictions() {
    if (_start.day() < 1 || _start.day() > 28) {
        throw ghoul::RuntimeError(fmt::format(
            "Invalid start day value of {} for day of month. Valid days are 1 - 28",
            _start.day()
        ));
    }
    if (_start.hour() != 0 || _start.minute() != 0 || _start.second() != 0) {
        throw ghoul::RuntimeError(fmt::format(
            "Invalid start time value of {}:{}:{}. Time must be 00:00:00",
            _start.hour(), _start.minute(), _start.second()
        ));
    }
}

void TimeQuantizer::verifyResolutionRestrictions(const int value, const char unit) {
    switch (unit) {
        case 'y':
            break;
        case 'M':
            if (value < 1 || value > 6) {
                throw ghoul::RuntimeError(fmt::format(
                    "Invalid resolution count of {} for (M)onth option. Valid counts are "
                    "1, 2, 3, 4, or 6", value
                ));
            }
            break;
        case 'd':
            if (value < 1 || value > 28) {
                throw ghoul::RuntimeError(fmt::format(
                    "Invalid resolution count of {} for (d)ay option. Valid counts are "
                    "1 - 28", value
                ));
            }
            break;
        case 'h':
            if (!(value >= 1 && value <= 4 || value == 6 || value == 12)) {
                throw ghoul::RuntimeError(fmt::format(
                    "Invalid resolution count of {} for (h)our option. Valid counts are "
                    "1, 2, 3, 4, 6, or 12", value
                ));
            }
            break;
        case 'm':
            if (value != 15 && value != 30) {
                throw ghoul::RuntimeError(fmt::format(
                    "Invalid resolution count of {} for (m)inute option. Valid counts "
                    "are 15 or 30", value
                ));
            }
            break;
        default:
            throw ghoul::RuntimeError(fmt::format(
                "Invalid unit format '{}'. Expected 'y', 'M', 'd', 'h', or 'm'", unit
            ));
    }
}

double TimeQuantizer::computeSecondsFromResolution(const int valueIn, const char unit) {
    double value = static_cast<double>(valueIn);
    // convert value to seconds, based on unit.
    // The switch statment has intentional fall throughs
    switch (unit) {
        case 'y':
            value *= 365;
            [[fallthrough]];
        case 'd':
            value *= 24.0;
            [[fallthrough]];
        case 'h':
            value *= 60.0;
            [[fallthrough]];
        case 'm':
            value *= 60.0;
            [[fallthrough]];
        case 's':
            value *= 1.0;
            break;

        case 'M':
            value *= (30.4 * 24.0 * 60.0 * 60.0);
            break;

        default:
            throw ghoul::RuntimeError(fmt::format(
                "Invalid resolution unit format '{}'. Expected 'y', 'M', 'd', 'h', 'm', "
                "or 's'", unit
            ));
    }
    return value;
}

bool TimeQuantizer::quantize(Time& t, bool clamp) {
    ZoneScoped

    constexpr const char Format[] = "YYYY-MM-DDTHR:MN:SC.###";
    constexpr const int BufferSize = sizeof(Format);
    char unquantizedString[BufferSize];
    std::memset(unquantizedString, 0, BufferSize);
    SpiceManager::ref().dateFromEphemerisTime(
        t.j2000Seconds(),
        unquantizedString, BufferSize,
        Format
    );

    DateTime unquantized(std::string_view(unquantizedString, BufferSize));
    // resolutionFraction helps to improve iteration performance
    constexpr const double ResolutionFraction = 0.7;
    constexpr const int IterationLimit = 50;
    int iterations = 0;
    int lastIncr = 0;
    int lastDecr = 0;

    if (_timerange.includes(t)) {
        DateTime quantized = DateTime(_timerange.start());
        doFirstApproximation(quantized, unquantized, _resolutionValue, _resolutionUnit);
        double error = unquantized.J2000() - quantized.J2000();
        while (error > (_resolution * ResolutionFraction) || error < 0) {
            if (error > 0) {
                lastIncr = quantized.increment(
                    static_cast<int>(_resolutionValue),
                    _resolutionUnit,
                    error,
                    _resolution
                );
            }
            else if (error < 0) {
                lastDecr = quantized.decrement(
                    static_cast<int>(_resolutionValue),
                    _resolutionUnit,
                    error,
                    _resolution
                );
            }
            error = unquantized.J2000() - quantized.J2000();
            bool hasSettled = (lastIncr == 1 && lastDecr == 1 && error >= 0.0);
            iterations++;
            if (hasSettled || iterations > IterationLimit) {
                break;
            }
        }
        char Buffer[20];
        std::memset(Buffer, 0, 20);
        fmt::format_to(
            Buffer,
            "{:0>4}-{:0>2}-{:0>2}T{:0>2}:{:0>2}:{:0>2}",
            quantized.year(), quantized.month(), quantized.day(),
            quantized.hour(), quantized.minute(), quantized.second()
        );

        t.setTime(_timerange.clamp(Buffer));
        return true;
    }
    else if (clamp) {
        t.setTime(_timerange.clamp(unquantizedString));
        return true;
    }
    else {
        return false;
    }
}

void TimeQuantizer::doFirstApproximation(DateTime& quantized, DateTime& unQ, double value,
                                         char unit)
{
    ZoneScoped

    switch (unit) {
        case 'y':
        {
            const double minYearsToAdjust = static_cast<double>(
                unQ.year()) - static_cast<double>(_start.year()
            );
            const double minIncrementsAdjust = minYearsToAdjust / value;
            quantized.setYear(
                _start.year() + static_cast<int>(minIncrementsAdjust * value)
            );
            break;
        }
        case 'M':
        {
            bool isSimMonthPastQuantizedMonth = unQ.month() > static_cast<int>(value);
            quantized.setYear(
                (isSimMonthPastQuantizedMonth) ? unQ.year() : unQ.year() - 1
            );
            break;
        }
        case 'd':
        {
            const double error = (unQ.J2000() - quantized.J2000()) / (60.0 * 60.0 * 24.0);
            const int originalHour = quantized.hour();
            const int originalMinute = quantized.minute();
            const int originalSecond = quantized.second();
            const double addToTime = std::round(error) * 86400;
            Time testDay(quantized.J2000() + addToTime);

            char Buffer[25];
            testDay.ISO8601(Buffer);
            quantized.setTime(std::string_view(Buffer, 25));
            quantized.setHour(originalHour);
            quantized.setMinute(originalMinute);
            quantized.setSecond(originalSecond);
            break;
        }
        case 'h':
        {
            quantized = unQ;
            quantized.setMinute(0);
            quantized.setSecond(0);
            if (unQ.hour() >= 12) {
                quantized.setHour(0);
            }
            else {
                quantized.decrementOnce(1, 'd');
            }
            break;
        }
        case 'm':
            quantized = unQ;
            quantized.setMinute(0);
            quantized.setSecond(0);
            if (quantized.hour() > 0) {
                quantized.decrementOnce(1, 'h');
            }
            else {
                quantized.decrementOnce(1, 'd');
            }
            break;
        default:
            throw ghoul::RuntimeError(fmt::format(
                "Invalid unit '{}'. Expected 'y', 'M', 'd', 'h', or 'm'", unit
            ));
    }
}

std::vector<std::string> TimeQuantizer::quantized(Time& start, Time& end) {
    DateTime s(start.ISO8601());
    quantize(start, true);

    DateTime e(end.ISO8601());
    quantize(end, true);

    const double startSeconds = s.J2000();
    const double endSeconds = e.J2000();
    const double delta = endSeconds - startSeconds;

    ghoul_assert(
        static_cast<int>(delta) % static_cast<int>(_resolution) == 0,
        "Quantization error"
    );
    const int nSteps = static_cast<int>(delta / _resolution);

    std::vector<std::string> result;
    DateTime itr = s;
    RangedTime range(std::string(start.ISO8601()), std::string(end.ISO8601()));
    while (range.includes(Time(itr.ISO8601()))) {
        itr.incrementOnce(static_cast<int>(_resolutionValue), _resolutionUnit);
        result.push_back(itr.ISO8601());
    }

    return result;
}

} // namespace openspace::globebrowsing
