/*****************************************************************************************
 *                                                                                       *
 * 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 <openspace/engine/globals.h>
#include <openspace/rendering/renderengine.h>
#include <openspace/scene/scene.h>
#include <openspace/util/timeconversion.h>

#include <ghoul/fmt.h>
#include <ghoul/misc/assert.h>
#include <ghoul/misc/misc.h>
#include <cctype>
#include <ctime>

namespace openspace::luascriptfunctions {

/**
 * \ingroup LuaScripts
 * setDeltaTime(number):
 * Sets the delta time by calling the Time::setDeltaTime method
 */
int time_setDeltaTime(lua_State* L) {
    const int nArguments = lua_gettop(L);
    if (nArguments == 1) {
        const bool isNumber = (lua_isnumber(L, 1) != 0);
        if (!isNumber) {
            lua_settop(L, 0);
            const char* msg = lua_pushfstring(
                L,
                "%s expected, got %s",
                lua_typename(L, LUA_TNUMBER),
                luaL_typename(L, -1)
            );
            return luaL_error(L, "bad argument #%d (%s)", 2, msg);
        }
        const double newDeltaTime = lua_tonumber(L, 1);
        global::timeManager.setDeltaTime(newDeltaTime);
    }
    else {
        lua_settop(L, 0);
        const char* msg = lua_pushfstring(L,
            "Bad number of arguments. Expected 1 or 2.");
        return ghoul::lua::luaError(L, fmt::format("bad argument ({})", msg));
    }

    lua_settop(L, 0);
    ghoul_assert(lua_gettop(L) == 0, "Incorrect number of items left on stack");
    return 0;
}

/**
 * \ingroup LuaScripts
* interpolateNextDeltaTimeStep(list of numbers):
* Sets the list of discrete delta time steps for the simulation speed.
 */
int time_setDeltaTimeSteps(lua_State* L) {
    ghoul::lua::checkArgumentsAndThrow(L, 1, "lua::time_setDeltaTimeSteps");

    ghoul::Dictionary dict;
    ghoul::lua::luaDictionaryFromState(L, dict);
    const size_t nItems = dict.size();

    std::vector<double> inputDeltaTimes;
    inputDeltaTimes.reserve(nItems);

    for (size_t i = 1; i <= nItems; ++i) {
        std::string key = std::to_string(i);
        if (dict.hasKeyAndValue<double>(key)) {
            const double time = dict.value<double>(key);
            inputDeltaTimes.push_back(time);
        }
        else {
            const char* msg = lua_pushfstring(L,
                "Error setting delta times. Expected list of numbers.");
            return ghoul::lua::luaError(L, fmt::format("bad argument ({})", msg));
        }
    }
    lua_pop(L, 1);

    global::timeManager.setDeltaTimeSteps(inputDeltaTimes);

    lua_settop(L, 0);
    ghoul_assert(lua_gettop(L) == 0, "Incorrect number of items left on stack");
    return 0;
}

/**
 * \ingroup LuaScripts
* setNextDeltaTimeStep():
* Immediately set the simulation speed to the first delta time step in the list that is 
* larger than the current choice of simulation speed, if any.
 */
int time_setNextDeltaTimeStep(lua_State* L) {
    ghoul::lua::checkArgumentsAndThrow(L, 0, "lua::time_setNextDeltaTimeStep");

    global::timeManager.setNextDeltaTimeStep();

    lua_settop(L, 0);
    ghoul_assert(lua_gettop(L) == 0, "Incorrect number of items left on stack");
    return 0;
}

/**
 * \ingroup LuaScripts
* setPreviousDeltaTimeStep():
* Immediately set the simulation speed to the first delta time step in the list that is
* smaller than the current choice of simulation speed, if any.
 */
int time_setPreviousDeltaTimeStep(lua_State* L) {
    ghoul::lua::checkArgumentsAndThrow(L, 0, "lua::time_setPreviousDeltaTimeStep");

    global::timeManager.setPreviousDeltaTimeStep();

    lua_settop(L, 0);
    ghoul_assert(lua_gettop(L) == 0, "Incorrect number of items left on stack");
    return 0;
}

/**
 * \ingroup LuaScripts
* interpolateNextDeltaTimeStep([interpolationDuration]):
* Interpolate the simulation speed to the next delta time step in the list. If an input
* value is given, the interpolation is done over the specified number of seconds.
* If interpolationDuration is not provided, the interpolation time will be based on the
* `defaultDeltaTimeInterpolationDuration` property of the TimeManager.
 */
int time_interpolateNextDeltaTimeStep(lua_State* L) {
    ghoul::lua::checkArgumentsAndThrow(
        L, 
        { 0, 1 }, 
        "lua::time_interpolateNextDeltaTimeStep"
    );

    double interpolationDuration = 
        global::timeManager.defaultDeltaTimeInterpolationDuration();

    const int nArguments = lua_gettop(L);
    if (nArguments == 1) {
        const bool durationIsNumber = (lua_isnumber(L, 1) != 0);
        if (!durationIsNumber) {
            lua_settop(L, 0);
            const char* msg = lua_pushfstring(
                L,
                "%s expected, got %s",
                lua_typename(L, LUA_TNUMBER),
                luaL_typename(L, -1)
            );
            return luaL_error(L, "bad argument #%d (%s)", 1, msg);
        }
        interpolationDuration = lua_tonumber(L, 1);
    }

    global::timeManager.interpolateNextDeltaTimeStep(interpolationDuration);

    lua_settop(L, 0);
    ghoul_assert(lua_gettop(L) == 0, "Incorrect number of items left on stack");
    return 0;
}

/**
 * \ingroup LuaScripts
* interpolatePreviousDeltaTimeStep([interpolationDuration]):
* Interpolate the simulation speed to the previous delta time step in the list. If an 
* input value is given, the interpolation is done over the specified number of seconds.
* If interpolationDuration is not provided, the interpolation time will be based on the
* `defaultDeltaTimeInterpolationDuration` property of the TimeManager.
 */
int time_interpolatePreviousDeltaTimeStep(lua_State* L) {
    ghoul::lua::checkArgumentsAndThrow(
        L, 
        { 0, 1 }, 
        "lua::time_interpolatePreviousDeltaTimeStep"
    );

    double interpolationDuration = 
        global::timeManager.defaultDeltaTimeInterpolationDuration();

    const int nArguments = lua_gettop(L);
    if (nArguments == 1) {
        const bool durationIsNumber = (lua_isnumber(L, 1) != 0);
        if (!durationIsNumber) {
            lua_settop(L, 0);
            const char* msg = lua_pushfstring(
                L,
                "%s expected, got %s",
                lua_typename(L, LUA_TNUMBER),
                luaL_typename(L, -1)
            );
            return luaL_error(L, "bad argument #%d (%s)", 1, msg);
        }
        interpolationDuration = lua_tonumber(L, 1);
    }

    global::timeManager.interpolatePreviousDeltaTimeStep(interpolationDuration);

    lua_settop(L, 0);
    ghoul_assert(lua_gettop(L) == 0, "Incorrect number of items left on stack");
    return 0;
}

/**
* \ingroup LuaScripts
* time_interpolateDeltaTime(number [, number]):
* Interpolates the delta time by calling the Time::interpolateDeltaTime method
* Same behaviour as setDeltaTime, but interpolates the delta time.
* If interpolationDuration is not provided, the interpolation time will be based on the
* `defaultDeltaTimeInterpolationDuration` property of the TimeManager.
*/
int time_interpolateDeltaTime(lua_State* L) {
    const int nArguments = lua_gettop(L);
    if (nArguments == 2) {
        const bool deltaIsNumber = (lua_isnumber(L, 1) != 0);
        if (!deltaIsNumber) {
            lua_settop(L, 0);
            const char* msg = lua_pushfstring(
                L,
                "%s expected, got %s",
                lua_typename(L, LUA_TNUMBER),
                luaL_typename(L, -1)
            );
            return luaL_error(L, "bad argument #%d (%s)", 2, msg);
        }

        const bool durationIsNumber = (lua_isnumber(L, 2) != 0);
        if (!durationIsNumber) {
            lua_settop(L, 0);
            const char* msg = lua_pushfstring(
                L,
                "%s expected, got %s",
                lua_typename(L, LUA_TNUMBER),
                luaL_typename(L, -1)
            );
            return luaL_error(L, "bad argument #%d (%s)", 2, msg);
        }

        const double interpolationDuration = lua_tonumber(L, 2);
        const double newDeltaTime = lua_tonumber(L, 1);
        global::timeManager.interpolateDeltaTime(newDeltaTime, interpolationDuration);
    }
    else if (nArguments == 1) {
        const bool isNumber = (lua_isnumber(L, 1) != 0);
        if (!isNumber) {
            lua_settop(L, 0);
            const char* msg = lua_pushfstring(
                L,
                "%s expected, got %s",
                lua_typename(L, LUA_TNUMBER),
                luaL_typename(L, -1)
            );
            return luaL_error(L, "bad argument #%d (%s)", 2, msg);
        }
        const double newDeltaTime = lua_tonumber(L, 1);
        global::timeManager.interpolateDeltaTime(
            newDeltaTime,
            global::timeManager.defaultDeltaTimeInterpolationDuration()
        );
    }
    else {
        lua_settop(L, 0);
        const char* msg = lua_pushfstring(L,
            "Bad number of arguments. Expected 1 or 2.");
        return ghoul::lua::luaError(L, fmt::format("bad argument ({})", msg));
    }

    lua_settop(L, 0);
    ghoul_assert(lua_gettop(L) == 0, "Incorrect number of items left on stack");
    return 0;
}


/**
 * \ingroup LuaScripts
 * deltaTime():
 * Returns the delta time by calling the Time::deltaTime method
 */
int time_deltaTime(lua_State* L) {
    lua_pushnumber(L, global::timeManager.deltaTime());
    ghoul_assert(lua_gettop(L) == 1, "Incorrect number of items left on stack");
    return 1;
}

/**
 * \ingroup LuaScripts
 * togglePause():
 * Toggles pause, i.e. setting the delta time to 0 and restoring it afterwards
 */
int time_togglePause(lua_State* L) {
    const int nArguments = lua_gettop(L);

    if (nArguments == 0) {
        global::timeManager.setPause(!global::timeManager.isPaused());
    }
    else {
        lua_settop(L, 0);
        return luaL_error(
            L,
            "bad number of arguments, expected 0, got %i",
            nArguments
        );
    }

    lua_settop(L, 0);
    ghoul_assert(lua_gettop(L) == 0, "Incorrect number of items left on stack");
    return 0;
}

/**
* \ingroup LuaScripts
* interpolateTogglePause([interpolationDuration]):
* Same behaviour as togglePause, but with interpolation.
* If no interpolation duration is provided, the interpolation time will be based on the
* `defaultPauseInterpolationDuration` and `defaultUnpauseInterpolationDuration` properties
* of the TimeManager.
*/
int time_interpolateTogglePause(lua_State* L) {
    const int nArguments = lua_gettop(L);

    if (nArguments == 1) {
        const bool isNumber = (lua_isnumber(L, 1) != 0);
        if (!isNumber) {
            const char* msg = lua_pushfstring(
                L,
                "%s expected, got %s",
                lua_typename(L, LUA_TNUMBER),
                luaL_typename(L, -1)
            );
            return luaL_error(L, "bad argument #%d (%s)", 1, msg);
        }

        const double interpolationDuration = lua_tonumber(L, 1);

        global::timeManager.interpolatePause(
            !global::timeManager.isPaused(),
            interpolationDuration
        );
    }
    else if (nArguments == 0) {
        const bool pause = !global::timeManager.isPaused();
        global::timeManager.interpolatePause(pause,
            pause ?
            global::timeManager.defaultPauseInterpolationDuration() :
            global::timeManager.defaultUnpauseInterpolationDuration()
        );
    }
    else {
        lua_settop(L, 0);
        return luaL_error(
            L,
            "bad number of arguments, expected 0 or 1, got %i",
            nArguments
        );
    }

    lua_settop(L, 0);
    ghoul_assert(lua_gettop(L) == 0, "Incorrect number of items left on stack");
    return 0;
}


/**
 * \ingroup LuaScripts
 * togglePause():
 * Toggles a pause function i.e. setting the delta time to 0 and restoring it afterwards
 */
int time_setPause(lua_State* L) {
    const int nArguments = lua_gettop(L);

    if (nArguments == 1) {
        const bool pause = lua_toboolean(L, 1) == 1;
        global::timeManager.setPause(pause);
    }
    else {
        lua_settop(L, 0);
        return luaL_error(
            L,
            "bad number of arguments, expected 1, got %i",
            nArguments
        );
    }

    lua_settop(L, 0);
    ghoul_assert(lua_gettop(L) == 0, "Incorrect number of items left on stack");
    return 0;
}

/**
* \ingroup LuaScripts
* interpolateTogglePause(bool [, interpolationDuration]):
* Same behaviour as setPause, but with interpolation.
* If no interpolation duration is provided, the interpolation time will be based on the
* `defaultPauseInterpolationDuration` and `defaultUnpauseInterpolationDuration` properties
* of the TimeManager.
*/
int time_interpolatePause(lua_State* L) {
    const int nArguments = lua_gettop(L);

    if (nArguments == 2) {
        const bool isNumber = (lua_isnumber(L, 2) != 0);
        if (!isNumber) {
            lua_settop(L, 0);
            const char* msg = lua_pushfstring(
                L,
                "%s expected, got %s",
                lua_typename(L, LUA_TNUMBER),
                luaL_typename(L, -1)
            );
            return luaL_error(L, "bad argument #%d (%s)", 2, msg);
        }
        const double interpolationDuration = lua_tonumber(L, 2);
        const bool pause = lua_toboolean(L, 1) == 1;
        global::timeManager.interpolatePause(pause, interpolationDuration);
    }
    else if (nArguments == 1) {
        const bool pause = lua_toboolean(L, 1) == 1;
        global::timeManager.interpolatePause(pause,
            pause ?
            global::timeManager.defaultPauseInterpolationDuration() :
            global::timeManager.defaultUnpauseInterpolationDuration()
        );
    }
    else {
        lua_settop(L, 0);
        return luaL_error(
            L,
            "bad number of arguments, expected 1 or 2, got %i",
            nArguments
        );
    }

    lua_settop(L, 0);
    ghoul_assert(lua_gettop(L) == 0, "Incorrect number of items left on stack");
    return 0;
}


/**
 * \ingroup LuaScripts
 * setTime({number, string}):
 * Sets the simulation time to the passed value. If the parameter is a number, it is
 * interpreted as the number of seconds past the J2000 epoch and the
 * Time::setTime(double) method is called. If the parameter is a string, it is
 * interpreted as a structured date string and the Time::setTime(std::string) method
 * is called
 */
int time_setTime(lua_State* L) {
    const bool isFunction = (lua_isfunction(L, -1) != 0);
    if (isFunction) {
        // If the top of the stack is a function, it is ourself
        const char* msg = lua_pushfstring(L, "method called without argument");
        return ghoul::lua::luaError(L, fmt::format("bad argument #1 ({})", msg));
    }

    const bool isNumber = (lua_isnumber(L, 1) != 0);
    const bool isString = (lua_isstring(L, 1) != 0);
    if (!isNumber && !isString) {
        const char* msg = lua_pushfstring(
            L,
            "%s or %s expected, got %s",
            lua_typename(L, LUA_TNUMBER),
            lua_typename(L, LUA_TSTRING),
            luaL_typename(L, -1)
        );
        return ghoul::lua::luaError(L, fmt::format("bad argument #1 ({})", msg));
    }

    const int nArguments = lua_gettop(L);
    if (nArguments == 1) {
        if (isNumber) {
            double value = lua_tonumber(L, 1);
            global::timeManager.setTimeNextFrame(Time(value));
            return 0;
        }
        if (isString) {
            const char* time = lua_tostring(L, 1);
            global::timeManager.setTimeNextFrame(Time(Time::convertTime(time)));
            return 0;
        }
        ghoul_assert(lua_gettop(L) == 0, "Incorrect number of items left on stack");
    }
    else {
        return luaL_error(
            L,
            "bad number of arguments, expected 1 or 2, got %i",
            nArguments
        );
    }
    return 0;
}


/**
* \ingroup LuaScripts
* interpolateTime({number, string} [, interpolationDuration]):
* Interpolates the simulation time to the passed value.
* Same behaviour as setTime, but interpolates time.
* If interpolationDuration is not provided, the interpolation time will be based on the
* `defaultTimeInterpolationDuration` property of the TimeManager.
*/
int time_interpolateTime(lua_State* L) {
    const bool isFunction = (lua_isfunction(L, -1) != 0);
    if (isFunction) {
        // If the top of the stack is a function, it is ourself
        const char* msg = lua_pushfstring(L, "method called without argument");
        return ghoul::lua::luaError(L, fmt::format("bad argument #1 ({})", msg));
    }

    const bool isNumber = (lua_isnumber(L, 1) != 0);
    const bool isString = (lua_isstring(L, 1) != 0);
    if (!isNumber && !isString) {
        const char* msg = lua_pushfstring(
            L,
            "%s or %s expected, got %s",
            lua_typename(L, LUA_TNUMBER),
            lua_typename(L, LUA_TSTRING),
            luaL_typename(L, -1)
        );
        return ghoul::lua::luaError(L, fmt::format("bad argument #1 ({})", msg));
    }

    if (lua_gettop(L) == 1) {
        if (isNumber) {
            double value = lua_tonumber(L, 1);
            global::timeManager.interpolateTime(
                value,
                global::timeManager.defaultTimeInterpolationDuration()
            );
            return 0;
        }
        if (isString) {
            const char* time = lua_tostring(L, 1);
            global::timeManager.interpolateTime(
                Time::convertTime(time),
                global::timeManager.defaultTimeInterpolationDuration()
            );
            return 0;
        }
        ghoul_assert(lua_gettop(L) == 0, "Incorrect number of items left on stack");
    }
    else {
        int nArguments = lua_gettop(L);
        if (nArguments != 2) {
            return luaL_error(
                L,
                "bad number of arguments, expected 1 or 2, got %i",
                nArguments
            );
        }

        double targetTime;
        if (lua_isnumber(L, 1)) {
            targetTime = lua_tonumber(L, 1);
        }
        else {
            targetTime = Time::convertTime(lua_tostring(L, 1));
        }

        const double duration = lua_tonumber(L, 2);
        if (duration > 0) {
            global::timeManager.interpolateTime(targetTime, duration);
        }
        else {
            global::timeManager.setTimeNextFrame(Time(targetTime));
        }
    }
    return 0;
}


/**
* \ingroup LuaScripts
* interpolateTimeRelative(number [, interpolationDuration]):
* Interpolates the simulation time relatively, based on the specified number of seconds.
* If interpolationDuration is not provided, the interpolation time will be based on the
* `defaultTimeInterpolationDuration` property of the TimeManager.
*/
int time_interpolateTimeRelative(lua_State* L) {
    const bool isFunction = (lua_isfunction(L, -1) != 0);
    if (isFunction) {
        // If the top of the stack is a function, it is ourself
        const char* msg = lua_pushfstring(L, "method called without argument");
        return ghoul::lua::luaError(L, fmt::format("bad argument #1 ({})", msg));
    }

    const bool isNumber = (lua_isnumber(L, 1) != 0);
    if (!isNumber) {
        const char* msg = lua_pushfstring(
            L,
            "%s or expected, got %s",
            lua_typename(L, LUA_TNUMBER),
            luaL_typename(L, -1)
        );
        return ghoul::lua::luaError(L, fmt::format("bad argument #1 ({})", msg));
    }

    if (lua_gettop(L) == 1 && isNumber) {
        double delta = lua_tonumber(L, 1);
        global::timeManager.interpolateTimeRelative(
            delta,
            global::timeManager.defaultTimeInterpolationDuration()
        );
        ghoul_assert(lua_gettop(L) == 0, "Incorrect number of items left on stack");

        return 0;
    }
    else {
        int nArguments = lua_gettop(L);
        if (nArguments != 2) {
            return luaL_error(
                L,
                "bad number of arguments, expected 1 or 2, got %i",
                nArguments
            );
        }

        double delta;
        delta = lua_tonumber(L, 1);

        const double duration = lua_tonumber(L, 2);
        if (duration > 0) {
            global::timeManager.interpolateTimeRelative(delta, duration);
        }
        else {
            global::timeManager.setTimeNextFrame(
                Time(global::timeManager.time().j2000Seconds() + delta)
            );
        }
    }
    return 0;
}

/**
 * \ingroup LuaScripts
 * currentTime():
 * Returns the current simulation time as the number of seconds past the J2000 epoch.
 * It is returned by calling the Time::currentTime method.
 */
int time_currentTime(lua_State* L) {
    ghoul::lua::push(L, global::timeManager.time().j2000Seconds());
    ghoul_assert(lua_gettop(L) == 1, "Incorrect number of items left on stack");
    return 1;
}

/**
 * \ingroup LuaScripts
 * UTC():
 * Returns the current simulation time as a structured ISO 8601 string using the UTC
 * timezone by calling the Time::UTC method
 */
int time_currentTimeUTC(lua_State* L) {
    ghoul::lua::push(L, global::timeManager.time().UTC());
    ghoul_assert(lua_gettop(L) == 1, "Incorrect number of items left on stack");
    return 1;
}

/**
 * \ingroup LuaScripts
 * currentWallTime():
 * Returns the current wallclock time as a structured ISO 8601 string in the UTC timezone.
 */
int time_currentWallTime(lua_State* L) {
    std::time_t t = std::time(nullptr);
    std::tm* utcTime = std::gmtime(&t);
    ghoul_assert(utcTime, "Conversion to UTC failed");

    std::string time = fmt::format(
        "{:04d}-{:02d}-{:02d}T{:02d}:{:02d}:{:02d}",
        utcTime->tm_year + 1900,
        utcTime->tm_mon + 1,
        utcTime->tm_mday,
        utcTime->tm_hour,
        utcTime->tm_min,
        utcTime->tm_sec
    );
    lua_pushstring(L, time.c_str());
    ghoul_assert(lua_gettop(L) == 1, "Incorrect number of items left on stack");
    return 1;
}

int time_advancedTime(lua_State* L) {
    ghoul::lua::checkArgumentsAndThrow(L, 2, "lua::time_advanceTime");

    double j2000Seconds = -1.0;
    Time t;
    bool usesISO = false;
    if (lua_type(L, 1) == LUA_TSTRING) {
        j2000Seconds = Time::convertTime(ghoul::lua::value<std::string>(L, 1));
        usesISO = true;
    }
    else if (lua_type(L, 1) == LUA_TNUMBER) {
        j2000Seconds = ghoul::lua::value<double>(L, 1);
        usesISO = false;
    }

    double dt = 0.0;
    if (lua_type(L, 2) == LUA_TNUMBER) {
        dt = ghoul::lua::value<double>(L, 2);
    }
    else {
        std::string modifier = ghoul::lua::value<std::string>(L, 2);
        if (modifier.empty()) {
            return ghoul::lua::luaError(L, "Modifier string must not be empty");
        }
        ghoul::trimWhitespace(modifier);
        bool isNegative = false;
        if (modifier[0] == '-') {
            isNegative = true;
            modifier = modifier.substr(1);
        }

        auto it = std::find_if(
            modifier.begin(),
            modifier.end(),
            [](unsigned char c) {
                const bool digit = std::isdigit(c) != 0;
                const bool isDot = c == '.';
                return !digit && !isDot;
            }
        );

        double value = std::stod(std::string(modifier.begin(), it));

        std::string unitName = std::string(it, modifier.end());

        TimeUnit unit = TimeUnit::Second;
        if (unitName == "s") {
            unit = TimeUnit::Second;
        }
        else if (unitName == "m") {
            unit = TimeUnit::Minute;
        }
        else if (unitName == "h") {
            unit = TimeUnit::Hour;
        }
        else if (unitName == "d") {
            unit = TimeUnit::Day;
        }
        else if (unitName == "M") {
            unit = TimeUnit::Month;
        }
        else if (unitName == "y") {
            unit = TimeUnit::Year;
        }
        else {
            return ghoul::lua::luaError(
                L,
                fmt::format("Unknown unit '{}'", unitName)
            );
        }

        dt = convertTime(value, unit, TimeUnit::Second);
        if (isNegative) {
            dt *= -1.0;
        }
    }

    lua_pop(L, 2);

    if (usesISO) {
        ghoul::lua::push(L, Time(j2000Seconds + dt).ISO8601());
    }
    else {
        ghoul::lua::push(L, j2000Seconds + dt);
    }
    return 1;
}

} // namespace openspace::luascriptfunctions
