/**
 * Copyright (c) 2015, Timothy Stack
 *
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * * Redistributions of source code must retain the above copyright notice, this
 * list of conditions and the following disclaimer.
 * * Redistributions in binary form must reproduce the above copyright notice,
 * this list of conditions and the following disclaimer in the documentation
 * and/or other materials provided with the distribution.
 * * Neither the name of Timothy Stack nor the names of its contributors
 * may be used to endorse or promote products derived from this software
 * without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

#include <unordered_set>

#include "relative_time.hh"

#include "base/lnav_log.hh"
#include "base/time_util.hh"
#include "config.h"
#include "pcrepp/pcre2pp.hh"
#include "scn/scn.h"

using namespace std::chrono_literals;

static const struct {
    const char* name;
    lnav::pcre2pp::code pcre;
} MATCHERS[relative_time::RTT__MAX] = {
    {
        "ws",
        lnav::pcre2pp::code::from_const("\\A\\s+\\b"),
    },
    {
        "am",
        lnav::pcre2pp::code::from_const("\\Aam|a\\.m\\.\\b"),
    },
    {
        "pm",
        lnav::pcre2pp::code::from_const("\\Apm|p\\.m\\.\\b"),
    },
    {
        "a",
        lnav::pcre2pp::code::from_const("\\Aa\\b"),
    },
    {
        "an",
        lnav::pcre2pp::code::from_const("\\Aan\\b"),
    },
    {
        "at",
        lnav::pcre2pp::code::from_const("\\Aat\\b"),
    },
    {
        "time",
        lnav::pcre2pp::code::from_const(
            "\\A(\\d{1,2}):(\\d{2})(?::(\\d{2})(?:\\.(\\d{3,6}))?)?"),
    },
    {
        "num",
        lnav::pcre2pp::code::from_const("\\A((?:-|\\+)?\\d+)"),
    },

    {
        "sun",
        lnav::pcre2pp::code::from_const("\\Asun(days?)?\\b"),
    },
    {
        "mon",
        lnav::pcre2pp::code::from_const("\\Amon(days?)?\\b"),
    },
    {
        "tue",
        lnav::pcre2pp::code::from_const("\\Atue(s(days?)?)?\\b"),
    },
    {
        "wed",
        lnav::pcre2pp::code::from_const("\\Awed(nesdays?)?\\b"),
    },
    {
        "thu",
        lnav::pcre2pp::code::from_const("\\Athu(rsdays?)?\\b"),
    },
    {
        "fri",
        lnav::pcre2pp::code::from_const("\\Afri(days?)?\\b"),
    },
    {
        "sat",
        lnav::pcre2pp::code::from_const("\\Asat(urdays?)?\\b"),
    },

    {
        "us",
        lnav::pcre2pp::code::from_const(
            "\\A(?:micros(?:econds?)?|us(?![a-zA-Z]))"),
    },
    {
        "ms",
        lnav::pcre2pp::code::from_const(
            "\\A(?:millis(?:econds?)?|ms(?![a-zA-Z]))"),
    },
    {
        "sec",
        lnav::pcre2pp::code::from_const("\\As(?:ec(?:onds?)?)?(?![a-zA-Z])"),
    },
    {
        "min",
        lnav::pcre2pp::code::from_const("\\Am(?:in(?:utes?)?)?(?![a-zA-Z])"),
    },
    {
        "h",
        lnav::pcre2pp::code::from_const("\\Ah(?:ours?)?(?![a-zA-Z])"),
    },
    {
        "day",
        lnav::pcre2pp::code::from_const("\\Ad(?:ays?)?(?![a-zA-Z])"),
    },
    {
        "week",
        lnav::pcre2pp::code::from_const("\\Aw(?:eeks?)?(?![a-zA-Z])"),
    },
    {
        "mon",
        lnav::pcre2pp::code::from_const("\\Amon(?:ths?)?(?![a-zA-Z])"),
    },
    {
        "year",
        lnav::pcre2pp::code::from_const("\\Ay(?:ears?)?(?![a-zA-Z])"),
    },
    {
        "today",
        lnav::pcre2pp::code::from_const("\\Atoday\\b"),
    },
    {
        "yest",
        lnav::pcre2pp::code::from_const("\\Ayesterday\\b"),
    },
    {
        "tomo",
        lnav::pcre2pp::code::from_const("\\Atomorrow\\b"),
    },
    {
        "noon",
        lnav::pcre2pp::code::from_const("\\Anoon\\b"),
    },
    {
        "and",
        lnav::pcre2pp::code::from_const("\\Aand\\b"),
    },
    {
        "the",
        lnav::pcre2pp::code::from_const("\\Athe\\b"),
    },
    {
        "ago",
        lnav::pcre2pp::code::from_const("\\Aago\\b"),
    },
    {
        "lter",
        lnav::pcre2pp::code::from_const("\\Alater\\b"),
    },
    {
        "bfor",
        lnav::pcre2pp::code::from_const("\\Abefore\\b"),
    },
    {
        "aft",
        lnav::pcre2pp::code::from_const("\\Aafter\\b"),
    },
    {
        "now",
        lnav::pcre2pp::code::from_const("\\Anow\\b"),
    },
    {
        "here",
        lnav::pcre2pp::code::from_const("\\Ahere\\b"),
    },
    {
        "next",
        lnav::pcre2pp::code::from_const("\\Anext\\b"),
    },
    {
        "previous",
        lnav::pcre2pp::code::from_const("\\A(?:previous\\b|last\\b)"),
    },
};

static int64_t TIME_SCALES[] = {
    1000 * 1000,
    60,
    60,
    24,
};

const char relative_time::FIELD_CHARS[] = {
    'u',
    's',
    'm',
    'h',
    'd',
    'M',
    'y',
};

Result<relative_time, relative_time::parse_error>
relative_time::from_str(string_fragment str)
{
    int64_t number = 0;
    bool number_set = false, number_was_set = false;
    bool next_set = false;
    token_t base_token = RTT_INVALID;
    rt_field_type last_field_type = RTF__MAX;
    relative_time retval;
    parse_error pe_out;
    std::unordered_set<int> seen_tokens;

    pe_out.pe_column = 0;
    pe_out.pe_msg.clear();

    auto remaining = str;
    while (true) {
        rt_field_type curr_field_type = RTF__MAX;

        if (remaining.empty()) {
            if (number_set) {
                if (number > 1970 && number < 2050) {
                    retval.rt_field[RTF_YEARS] = number - 1900;
                    retval.rt_absolute_field_end = RTF__MAX;

                    switch (base_token) {
                        case RTT_BEFORE: {
                            auto epoch = retval.to_timeval();
                            retval.rt_duration
                                = std::chrono::duration_cast<
                                      std::chrono::microseconds>(
                                      std::chrono::seconds(epoch.tv_sec))
                                + std::chrono::microseconds(epoch.tv_usec);
                            retval.rt_field[RTF_YEARS] = 70;
                            break;
                        }
                        case RTT_AFTER:
                            retval.rt_duration = std::chrono::duration_cast<
                                std::chrono::microseconds>(
                                std::chrono::hours(24 * 365 * 200));
                            break;
                        default:
                            break;
                    }
                    return Ok(retval);
                }

                pe_out.pe_msg = "Number given without a time unit";
                return Err(pe_out);
            }

            if (base_token != RTT_INVALID) {
                switch (base_token) {
                    case RTT_BEFORE:
                        pe_out.pe_msg
                            = "'before' requires a point in time (e.g. before "
                              "10am)";
                        break;
                    case RTT_AFTER:
                        pe_out.pe_msg
                            = "'after' requires a point in time (e.g. after "
                              "10am)";
                        break;
                    default:
                        ensure(false);
                        break;
                }
                return Err(pe_out);
            }

            retval.rollover();
            return Ok(retval);
        }

        bool found = false;
        for (int lpc = 0; lpc < RTT__MAX && !found; lpc++) {
            static thread_local auto md
                = lnav::pcre2pp::match_data::unitialized();

            token_t token = (token_t) lpc;
            auto match_res = MATCHERS[lpc]
                                 .pcre.capture_from(remaining)
                                 .into(md)
                                 .matches()
                                 .ignore_error();
            if (!match_res) {
                continue;
            }

            remaining = match_res->f_remaining;
            pe_out.pe_column = match_res->f_all.sf_begin;
            found = true;
            if (RTT_MICROS <= token && token <= RTT_YEARS) {
                if (!number_set) {
                    if (base_token != RTT_INVALID) {
                        base_token = RTT_INVALID;
                        retval.rt_absolute_field_end = RTF__MAX;
                        continue;
                    }
                    if (!retval.rt_next && !retval.rt_previous) {
                        pe_out.pe_msg = "Expecting a number before time unit";
                        return Err(pe_out);
                    }
                }
                number_was_set = number_set;
                number_set = false;
            }
            switch (token) {
                case RTT_YESTERDAY:
                case RTT_TODAY:
                case RTT_NOW: {
                    if (seen_tokens.count(token) > 0) {
                        pe_out.pe_msg
                            = "Current time reference has already been used";
                        return Err(pe_out);
                    }

                    seen_tokens.insert(RTT_YESTERDAY);
                    seen_tokens.insert(RTT_TODAY);
                    seen_tokens.insert(RTT_NOW);

                    struct timeval tv;
                    struct exttm tm;

                    gettimeofday(&tv, nullptr);
                    localtime_r(&tv.tv_sec, &tm.et_tm);
                    tm.et_nsec = tv.tv_usec * 1000;
                    tm = retval.adjust(tm);

                    retval.rt_field[RTF_YEARS] = tm.et_tm.tm_year;
                    retval.rt_field[RTF_MONTHS] = tm.et_tm.tm_mon;
                    retval.rt_field[RTF_DAYS] = tm.et_tm.tm_mday;
                    switch (token) {
                        case RTT_NOW:
                            retval.rt_field[RTF_HOURS] = tm.et_tm.tm_hour;
                            retval.rt_field[RTF_MINUTES] = tm.et_tm.tm_min;
                            retval.rt_field[RTF_SECONDS] = tm.et_tm.tm_sec;
                            retval.rt_field[RTF_MICROSECONDS]
                                = tm.et_nsec / 1000;
                            break;
                        case RTT_YESTERDAY:
                            retval.rt_field[RTF_DAYS].value -= 1;
                        case RTT_TODAY:
                            retval.rt_field[RTF_HOURS] = 0;
                            retval.rt_field[RTF_MINUTES] = 0;
                            retval.rt_field[RTF_SECONDS] = 0;
                            retval.rt_field[RTF_MICROSECONDS] = 0;
                            break;
                        default:
                            break;
                    }
                    retval.rt_absolute_field_end = RTF__MAX;
                    break;
                }
                case RTT_INVALID:
                case RTT_WHITE:
                case RTT_AND:
                case RTT_THE:
                    curr_field_type = last_field_type;
                    break;
                case RTT_AM:
                case RTT_PM:
                    if (seen_tokens.count(token) > 0) {
                        pe_out.pe_msg = "Time has already been set";
                        return Err(pe_out);
                    }
                    seen_tokens.insert(RTT_AM);
                    seen_tokens.insert(RTT_PM);
                    if (number_set) {
                        retval.rt_field[RTF_HOURS] = number;
                        retval.rt_field[RTF_MINUTES] = 0;
                        retval.rt_field[RTF_SECONDS] = 0;
                        retval.rt_field[RTF_MICROSECONDS] = 0;
                        retval.rt_duration = 1min;
                        retval.rt_absolute_field_end = RTF__MAX;
                        number_set = false;
                    }
                    if (!retval.is_absolute(RTF_YEARS)) {
                        pe_out.pe_msg
                            = "Expecting absolute time with A.M. or P.M.";
                        return Err(pe_out);
                    }
                    if (token == RTT_AM) {
                        if (retval.rt_field[RTF_HOURS].value == 12) {
                            retval.rt_field[RTF_HOURS] = 0;
                        }
                    } else if (retval.rt_field[RTF_HOURS].value < 12) {
                        retval.rt_field[RTF_HOURS].value += 12;
                    }
                    if (base_token == RTT_AFTER) {
                        std::chrono::microseconds usecs = 0s;
                        uint64_t carry = 0;

                        if (retval.rt_field[RTF_MICROSECONDS].value > 0) {
                            usecs += std::chrono::microseconds(
                                1000000ULL
                                - retval.rt_field[RTF_MICROSECONDS].value);
                            carry = 1;
                        }
                        if (carry || retval.rt_field[RTF_SECONDS].value > 0) {
                            usecs += std::chrono::seconds(
                                60 - carry
                                - retval.rt_field[RTF_SECONDS].value);
                            carry = 1;
                        }
                        if (carry || retval.rt_field[RTF_MINUTES].value > 0) {
                            usecs += std::chrono::minutes(
                                60 - carry
                                - retval.rt_field[RTF_MINUTES].value);
                            carry = 1;
                        }
                        usecs += std::chrono::hours(
                            24 - retval.rt_field[RTF_HOURS].value);
                        retval.rt_duration = usecs;
                    }
                    if (base_token == RTT_BEFORE) {
                        retval.rt_duration
                            = std::chrono::hours(
                                  retval.rt_field[RTF_HOURS].value)
                            + std::chrono::minutes(
                                  retval.rt_field[RTF_MINUTES].value)
                            + std::chrono::seconds(
                                  retval.rt_field[RTF_SECONDS].value)
                            + std::chrono::microseconds(
                                  retval.rt_field[RTF_MICROSECONDS].value);
                        retval.rt_field[RTF_HOURS].value = 0;
                        retval.rt_field[RTF_MINUTES].value = 0;
                        retval.rt_field[RTF_SECONDS].value = 0;
                        retval.rt_field[RTF_MICROSECONDS].value = 0;
                    }
                    base_token = RTT_INVALID;
                    break;
                case RTT_A:
                case RTT_AN:
                    number = 1;
                    number_set = true;
                    break;
                case RTT_AT:
                    break;
                case RTT_TIME: {
                    const auto hstr = md[1]->to_string();
                    const auto mstr = md[2]->to_string();
                    retval.rt_field[RTF_HOURS] = atoi(hstr.c_str());
                    retval.rt_field[RTF_MINUTES] = atoi(mstr.c_str());
                    if (md[3]) {
                        const auto sstr = md[3]->to_string();
                        retval.rt_field[RTF_SECONDS] = atoi(sstr.c_str());
                        if (md[4]) {
                            const auto substr = md[4]->to_string();

                            switch (substr.length()) {
                                case 3:
                                    retval.rt_field[RTF_MICROSECONDS]
                                        = atoi(substr.c_str()) * 1000;
                                    break;
                                case 6:
                                    retval.rt_field[RTF_MICROSECONDS]
                                        = atoi(substr.c_str());
                                    break;
                            }
                        } else {
                            retval.rt_field[RTF_MICROSECONDS].clear();
                            retval.rt_duration = 1s;
                        }
                    } else {
                        retval.rt_field[RTF_SECONDS].clear();
                        retval.rt_field[RTF_MICROSECONDS].clear();
                        retval.rt_duration = 1min;
                    }
                    retval.rt_absolute_field_end = RTF__MAX;
                    break;
                }
                case RTT_NUMBER: {
                    if (number_set) {
                        pe_out.pe_msg
                            = "No time unit given for the previous number";
                        return Err(pe_out);
                    }

                    auto num_scan_res
                        = scn::scan_value<int64_t>(md[0]->to_string_view());

                    if (!num_scan_res) {
                        pe_out.pe_msg = fmt::format(
                            FMT_STRING("Invalid number: {}"), md[0].value());
                        return Err(pe_out);
                    }
                    number = num_scan_res.value();
                    number_set = true;
                    break;
                }
                case RTT_MICROS:
                    retval.rt_field[RTF_MICROSECONDS] = number;
                    break;
                case RTT_MILLIS:
                    retval.rt_field[RTF_MICROSECONDS] = number * 1000;
                    break;
                case RTT_SECONDS:
                    if (number_was_set) {
                        retval.rt_field[RTF_SECONDS] = number;
                        curr_field_type = RTF_SECONDS;
                    } else if (next_set) {
                        retval.rt_field[RTF_MICROSECONDS] = 0;
                        retval.rt_absolute_field_end = RTF__MAX;
                    }
                    break;
                case RTT_MINUTES:
                    if (number_was_set) {
                        retval.rt_field[RTF_MINUTES] = number;
                        curr_field_type = RTF_MINUTES;
                    } else if (next_set) {
                        retval.rt_field[RTF_MICROSECONDS] = 0;
                        retval.rt_field[RTF_SECONDS] = 0;
                        retval.rt_absolute_field_end = RTF__MAX;
                    }
                    break;
                case RTT_HOURS:
                    if (number_was_set) {
                        retval.rt_field[RTF_HOURS] = number;
                        curr_field_type = RTF_HOURS;
                    } else if (next_set) {
                        retval.rt_field[RTF_MICROSECONDS] = 0;
                        retval.rt_field[RTF_SECONDS] = 0;
                        retval.rt_field[RTF_MINUTES] = 0;
                        retval.rt_absolute_field_end = RTF__MAX;
                    }
                    break;
                case RTT_DAYS:
                    if (number_was_set) {
                        retval.rt_field[RTF_DAYS] = number;
                        curr_field_type = RTF_DAYS;
                    } else if (next_set) {
                        retval.rt_field[RTF_MICROSECONDS] = 0;
                        retval.rt_field[RTF_SECONDS] = 0;
                        retval.rt_field[RTF_MINUTES] = 0;
                        retval.rt_field[RTF_HOURS] = 0;
                        retval.rt_absolute_field_end = RTF__MAX;
                    }
                    break;
                case RTT_WEEKS:
                    retval.rt_field[RTF_DAYS] = number * 7;
                    break;
                case RTT_MONTHS:
                    if (number_was_set) {
                        retval.rt_field[RTF_MONTHS] = number;
                        curr_field_type = RTF_MONTHS;
                    } else if (next_set) {
                        retval.rt_field[RTF_MICROSECONDS] = 0;
                        retval.rt_field[RTF_SECONDS] = 0;
                        retval.rt_field[RTF_MINUTES] = 0;
                        retval.rt_field[RTF_HOURS] = 0;
                        retval.rt_field[RTF_DAYS] = 0;
                        retval.rt_absolute_field_end = RTF__MAX;
                    }
                    break;
                case RTT_YEARS:
                    if (number_was_set) {
                        retval.rt_field[RTF_YEARS] = number;
                        curr_field_type = RTF_YEARS;
                    } else if (next_set) {
                        retval.rt_field[RTF_MICROSECONDS] = 0;
                        retval.rt_field[RTF_SECONDS] = 0;
                        retval.rt_field[RTF_MINUTES] = 0;
                        retval.rt_field[RTF_HOURS] = 0;
                        retval.rt_field[RTF_DAYS] = 0;
                        retval.rt_field[RTF_MONTHS] = 0;
                        retval.rt_absolute_field_end = RTF__MAX;
                    }
                    break;
                case RTT_AGO:
                    if (retval.empty()) {
                        pe_out.pe_msg = "Expecting a time unit";
                        return Err(pe_out);
                    }
                    for (int field = 0; field < RTF__MAX; field++) {
                        if (retval.rt_field[field].value > 0) {
                            retval.rt_field[field]
                                = -retval.rt_field[field].value;
                        }
                        if (last_field_type != RTF__MAX
                            && field < last_field_type)
                        {
                            retval.rt_field[field] = 0;
                        }
                    }
                    if (last_field_type != RTF__MAX) {
                        retval.rt_absolute_field_end = last_field_type;
                    }
                    break;
                case RTT_BEFORE:
                case RTT_AFTER:
                    if (base_token != RTT_INVALID) {
                        pe_out.pe_msg
                            = "Before/after ranges are not supported yet";
                        return Err(pe_out);
                    }
                    base_token = token;
                    break;
                case RTT_LATER:
                    if (retval.empty()) {
                        pe_out.pe_msg = "Expecting a time unit before 'later'";
                        return Err(pe_out);
                    }
                    break;
                case RTT_HERE:
                    break;
                case RTT_NEXT:
                    retval.rt_next = true;
                    next_set = true;
                    break;
                case RTT_PREVIOUS:
                    retval.rt_previous = true;
                    next_set = true;
                    break;
                case RTT_TOMORROW:
                    retval.rt_field[RTF_DAYS] = 1;
                    break;
                case RTT_NOON:
                    retval.rt_field[RTF_HOURS] = 12;
                    retval.rt_absolute_field_end = RTF__MAX;
                    for (int lpc2 = RTF_MICROSECONDS; lpc2 < RTF_HOURS; lpc2++)
                    {
                        retval.rt_field[lpc2] = 0;
                    }
                    break;

                case RTT_SUNDAY:
                case RTT_MONDAY:
                case RTT_TUESDAY:
                case RTT_WEDNESDAY:
                case RTT_THURSDAY:
                case RTT_FRIDAY:
                case RTT_SATURDAY:
                    if (retval.rt_duration == 0s) {
                        switch (base_token) {
                            case RTT_BEFORE:
                                if (token == RTT_SUNDAY) {
                                    pe_out.pe_msg
                                        = "Sunday is the start of the week, so "
                                          "there is nothing before it";
                                    return Err(pe_out);
                                }
                                for (int wday = RTT_SUNDAY; wday < token;
                                     wday++)
                                {
                                    retval.rt_included_days.insert(
                                        (token_t) wday);
                                }
                                break;
                            case RTT_AFTER:
                                if (token == RTT_SATURDAY) {
                                    pe_out.pe_msg
                                        = "Saturday is the end of the week, so "
                                          "there is nothing after it";
                                    return Err(pe_out);
                                }
                                for (int wday = RTT_SATURDAY; wday > token;
                                     wday--)
                                {
                                    retval.rt_included_days.insert(
                                        (token_t) wday);
                                }
                                break;
                            default:
                                retval.rt_included_days.insert(token);
                                break;
                        }
                        base_token = RTT_INVALID;
                    } else {
                        retval.rt_included_days.insert(token);
                    }
                    if (retval.rt_duration == 0s) {
                        retval.rt_duration = 24h;
                    }
                    break;

                case RTT__MAX:
                    ensure(false);
                    break;
            }

            if (token != RTT_NEXT && token != RTT_PREVIOUS
                && token != RTT_WHITE)
            {
                next_set = false;
            }

            number_was_set = false;
            seen_tokens.insert(token);
        }

        if (!found) {
            pe_out.pe_msg = "Unrecognized input";
            return Err(pe_out);
        }

        last_field_type = curr_field_type;
    }
}

void
relative_time::rollover()
{
    for (int lpc = 0; lpc < RTF_DAYS; lpc++) {
        if (!this->rt_field[lpc].is_set) {
            continue;
        }
        int64_t val = this->rt_field[lpc].value;
        this->rt_field[lpc].value = val % TIME_SCALES[lpc];
        this->rt_field[lpc + 1].value += val / TIME_SCALES[lpc];
        if (this->rt_field[lpc + 1].value) {
            this->rt_field[lpc + 1].is_set = true;
        }
    }
    if (std::abs(this->rt_field[RTF_DAYS].value) > 31) {
        int64_t val = this->rt_field[RTF_DAYS].value;
        this->rt_field[RTF_DAYS].value = val % 31;
        this->rt_field[RTF_MONTHS].value += val / 31;
        if (this->rt_field[RTF_MONTHS].value) {
            this->rt_field[RTF_MONTHS].is_set = true;
        }
    }
    if (std::abs(this->rt_field[RTF_MONTHS].value) > 12) {
        int64_t val = this->rt_field[RTF_MONTHS].value;
        this->rt_field[RTF_MONTHS].value = val % 12;
        this->rt_field[RTF_YEARS].value += val / 12;
        if (this->rt_field[RTF_YEARS].value) {
            this->rt_field[RTF_YEARS].is_set = true;
        }
    }
}

relative_time
relative_time::from_timeval(const struct timeval& tv)
{
    relative_time retval;

    retval.clear();
    retval.rt_field[RTF_SECONDS] = tv.tv_sec;
    retval.rt_field[RTF_MICROSECONDS] = tv.tv_usec;
    retval.rollover();

    return retval;
}

relative_time
relative_time::from_usecs(std::chrono::microseconds usecs)
{
    relative_time retval;

    retval.clear();
    retval.rt_field[RTF_MICROSECONDS] = usecs.count();
    retval.rollover();

    return retval;
}

std::string
relative_time::to_string() const
{
    static const char* DAYS[] = {
        "sun",
        "mon",
        "tue",
        "wed",
        "thu",
        "fri",
        "sat",
    };

    char dst[128] = "";
    char* pos = dst;

    if (this->is_absolute()) {
        for (const auto& day_token : this->rt_included_days) {
            pos += snprintf(pos,
                            sizeof(dst) - (pos - dst),
                            "%s ",
                            DAYS[day_token - RTT_SUNDAY]);
        }

        pos += snprintf(
            pos,
            sizeof(dst) - (pos - dst),
            "%s",
            this->rt_next ? "next " : (this->rt_previous ? "last " : ""));
        if (this->rt_field[RTF_YEARS].is_set
            && (this->rt_next || this->rt_previous
                || this->rt_field[RTF_YEARS].value != 0))
        {
            pos += snprintf(pos,
                            sizeof(dst) - (pos - dst),
                            "year %" PRId64 " ",
                            this->rt_field[RTF_YEARS].value);
        } else if ((this->rt_next || this->rt_previous)
                   && this->rt_field[RTF_MONTHS].is_set)
        {
            pos += snprintf(pos, sizeof(dst) - (pos - dst), "year ");
        }
        if (this->rt_field[RTF_MONTHS].is_set
            && (this->rt_next || this->rt_previous
                || this->rt_field[RTF_MONTHS].value != 0))
        {
            pos += snprintf(pos,
                            sizeof(dst) - (pos - dst),
                            "month %" PRId64 " ",
                            this->rt_field[RTF_MONTHS].value);
        } else if ((this->rt_next || this->rt_previous)
                   && this->rt_field[RTF_DAYS].is_set)
        {
            pos += snprintf(pos, sizeof(dst) - (pos - dst), "month ");
        }
        if (this->rt_field[RTF_DAYS].is_set
            && (this->rt_next || this->rt_previous
                || this->rt_field[RTF_DAYS].value != 0))
        {
            pos += snprintf(pos,
                            sizeof(dst) - (pos - dst),
                            "day %" PRId64 " ",
                            this->rt_field[RTF_DAYS].value);
        } else if ((this->rt_next || this->rt_previous)
                   && this->rt_field[RTF_HOURS].is_set)
        {
            pos += snprintf(pos, sizeof(dst) - (pos - dst), "day ");
        }
        pos += snprintf(pos,
                        sizeof(dst) - (pos - dst),
                        "%" PRId64 ":%02" PRId64,
                        this->rt_field[RTF_HOURS].value,
                        this->rt_field[RTF_MINUTES].value);
        if (this->rt_field[RTF_SECONDS].is_set
            && this->rt_field[RTF_SECONDS].value != 0)
        {
            pos += snprintf(pos,
                            sizeof(dst) - (pos - dst),
                            ":%.02" PRId64,
                            this->rt_field[RTF_SECONDS].value);
            if (this->rt_field[RTF_MICROSECONDS].is_set
                && this->rt_field[RTF_MICROSECONDS].value != 0)
            {
                pos += snprintf(pos,
                                sizeof(dst) - (pos - dst),
                                ".%.03" PRId64,
                                this->rt_field[RTF_MICROSECONDS].value / 1000);
            }
        }
    } else {
        for (int lpc = RTF__MAX - 1; lpc >= 0; lpc--) {
            if (this->rt_field[lpc].value == 0) {
                continue;
            }
            pos += snprintf(pos,
                            sizeof(dst) - (pos - dst),
                            "%" PRId64 "%c",
                            this->rt_field[lpc].value,
                            FIELD_CHARS[lpc]);
        }
    }

    if (dst[0] == '\0') {
        dst[0] = '0';
        dst[1] = 's';
        dst[2] = '\0';
    }

    return dst;
}

struct exttm
relative_time::adjust(const exttm& tm) const
{
    auto retval = tm;

    if (this->rt_field[RTF_MICROSECONDS].is_set
        && this->is_absolute(RTF_MICROSECONDS))
    {
        retval.et_nsec = this->rt_field[RTF_MICROSECONDS].value * 1000;
    } else {
        retval.et_nsec += this->rt_field[RTF_MICROSECONDS].value * 1000;
    }
    if (this->rt_field[RTF_SECONDS].is_set && this->is_absolute(RTF_SECONDS)) {
        if (this->rt_next
            && this->rt_field[RTF_SECONDS].value <= tm.et_tm.tm_sec)
        {
            retval.et_tm.tm_min += 1;
        }
        if (this->rt_previous
            && this->rt_field[RTF_SECONDS].value >= tm.et_tm.tm_sec)
        {
            retval.et_tm.tm_min -= 1;
        }
        retval.et_tm.tm_sec = this->rt_field[RTF_SECONDS].value;
    } else {
        retval.et_tm.tm_sec += this->rt_field[RTF_SECONDS].value;
    }
    if (this->rt_field[RTF_MINUTES].is_set && this->is_absolute(RTF_MINUTES)) {
        if (this->rt_next
            && this->rt_field[RTF_MINUTES].value <= tm.et_tm.tm_min)
        {
            retval.et_tm.tm_hour += 1;
        }
        if (this->rt_previous
            && (this->rt_field[RTF_MINUTES].value == 0
                || (this->rt_field[RTF_MINUTES].value >= tm.et_tm.tm_min)))
        {
            retval.et_tm.tm_hour -= 1;
        }
        retval.et_tm.tm_min = this->rt_field[RTF_MINUTES].value;
    } else {
        retval.et_tm.tm_min += this->rt_field[RTF_MINUTES].value;
    }
    if (this->rt_field[RTF_HOURS].is_set && this->is_absolute(RTF_HOURS)) {
        retval.et_tm.tm_yday = -1;
        if (this->rt_next
            && this->rt_field[RTF_HOURS].value <= tm.et_tm.tm_hour)
        {
            retval.et_tm.tm_mday += 1;
        }
        if (this->rt_previous
            && this->rt_field[RTF_HOURS].value >= tm.et_tm.tm_hour)
        {
            retval.et_tm.tm_mday -= 1;
        }
        retval.et_tm.tm_hour = this->rt_field[RTF_HOURS].value;
    } else {
        retval.et_tm.tm_hour += this->rt_field[RTF_HOURS].value;
    }
    if (this->rt_field[RTF_DAYS].is_set && this->is_absolute(RTF_DAYS)) {
        if (this->rt_next && this->rt_field[RTF_DAYS].value <= tm.et_tm.tm_mday)
        {
            retval.et_tm.tm_mon += 1;
        }
        if (this->rt_previous
            && this->rt_field[RTF_DAYS].value >= tm.et_tm.tm_mday)
        {
            retval.et_tm.tm_mon -= 1;
        }
        retval.et_tm.tm_yday = -1;
        retval.et_tm.tm_mday = this->rt_field[RTF_DAYS].value;
    } else {
        retval.et_tm.tm_yday = -1;
        retval.et_tm.tm_mday += this->rt_field[RTF_DAYS].value;
    }
    if (this->rt_field[RTF_MONTHS].is_set && this->is_absolute(RTF_MONTHS)) {
        if (this->rt_next
            && this->rt_field[RTF_MONTHS].value <= tm.et_tm.tm_mon)
        {
            retval.et_tm.tm_year += 1;
        }
        if (this->rt_previous
            && this->rt_field[RTF_MONTHS].value >= tm.et_tm.tm_mon)
        {
            retval.et_tm.tm_year -= 1;
        }
        retval.et_tm.tm_mon = this->rt_field[RTF_MONTHS].value;
    } else {
        retval.et_tm.tm_mon += this->rt_field[RTF_MONTHS].value;
    }
    if (this->rt_field[RTF_YEARS].is_set && this->is_absolute(RTF_YEARS)) {
        retval.et_tm.tm_year = this->rt_field[RTF_YEARS].value;
    } else {
        retval.et_tm.tm_year += this->rt_field[RTF_YEARS].value;
    }

    return retval;
}

std::optional<exttm>
relative_time::window_start(const struct exttm& tm) const
{
    auto retval = tm;

    if (this->is_relative()) {
        uint64_t us, remainder;

        auto tv = tm.to_timeval();
        us = (uint64_t) tv.tv_sec * 1000000ULL + (uint64_t) tv.tv_usec;
        remainder = us % this->to_microseconds();
        us -= remainder;

        tv.tv_sec = us / 1000000ULL;
        tv.tv_usec = us % 1000000ULL;

        retval.et_tm = *gmtime(&tv.tv_sec);
        retval.et_nsec = tv.tv_usec * 1000ULL;

        return retval;
    }

    bool clear = false;

    if (this->rt_field[RTF_YEARS].is_set) {
        if (this->rt_field[RTF_YEARS].value > tm.et_tm.tm_year) {
            return std::nullopt;
        }
        retval.et_tm.tm_year = this->rt_field[RTF_YEARS].value;
        clear = true;
    }

    if (this->rt_field[RTF_MONTHS].is_set) {
        if (this->rt_field[RTF_MONTHS].value > tm.et_tm.tm_mon) {
            return std::nullopt;
        }
        retval.et_tm.tm_yday = -1;
        retval.et_tm.tm_mon = this->rt_field[RTF_MONTHS].value;
        clear = true;
    } else if (clear) {
        retval.et_tm.tm_yday = -1;
        retval.et_tm.tm_mon = 0;
    }

    if (this->rt_field[RTF_DAYS].is_set) {
        if (this->rt_field[RTF_DAYS].value > tm.et_tm.tm_mday) {
            return std::nullopt;
        }
        retval.et_tm.tm_yday = -1;
        retval.et_tm.tm_mday = this->rt_field[RTF_DAYS].value;
        clear = true;
    } else if (clear) {
        retval.et_tm.tm_yday = -1;
        retval.et_tm.tm_mday = 1;
    }

    if (!this->rt_included_days.empty()) {
        auto iter = this->rt_included_days.find(
            (token_t) (RTT_SUNDAY + tm.et_tm.tm_wday));

        if (iter == this->rt_included_days.end()) {
            return std::nullopt;
        }
        clear = true;
    }

    if (this->rt_field[RTF_HOURS].is_set) {
        if (this->rt_field[RTF_HOURS].value > tm.et_tm.tm_hour) {
            return std::nullopt;
        }
        retval.et_tm.tm_hour = this->rt_field[RTF_HOURS].value;
        clear = true;
    } else if (clear) {
        retval.et_tm.tm_hour = 0;
    }

    if (this->rt_field[RTF_MINUTES].is_set) {
        if (this->rt_field[RTF_MINUTES].value > tm.et_tm.tm_min) {
            return std::nullopt;
        }
        retval.et_tm.tm_min = this->rt_field[RTF_MINUTES].value;
        clear = true;
    } else if (clear) {
        retval.et_tm.tm_min = 0;
    }

    if (this->rt_field[RTF_SECONDS].is_set) {
        if (this->rt_field[RTF_SECONDS].value > tm.et_tm.tm_sec) {
            return std::nullopt;
        }
        retval.et_tm.tm_sec = this->rt_field[RTF_SECONDS].value;
        clear = true;
    } else if (clear) {
        retval.et_tm.tm_sec = 0;
    }

    if (this->rt_field[RTF_MICROSECONDS].is_set) {
        if (this->rt_field[RTF_MICROSECONDS].value > tm.et_nsec / 1000) {
            return std::nullopt;
        }
        retval.et_nsec = this->rt_field[RTF_MICROSECONDS].value * 1000ULL;
        clear = true;
    } else if (clear) {
        retval.et_nsec = 0;
    }

    auto tv = tm.to_timeval();
    auto start_time = retval.to_timeval();
    auto end_time = relative_time::from_usecs(this->rt_duration)
                        .adjust(retval)
                        .to_timeval();

    if (tv < start_time || end_time < tv) {
        return std::nullopt;
    }

    return retval;
}

int64_t
relative_time::to_microseconds() const
{
    int64_t retval;

    if (this->is_absolute()) {
        struct exttm etm;

        memset(&etm, 0, sizeof(etm));
        etm.et_tm.tm_year = this->rt_field[RTF_YEARS].value;
        etm.et_tm.tm_mon = this->rt_field[RTF_MONTHS].value;
        if (this->rt_field[RTF_DAYS].is_set) {
            etm.et_tm.tm_mday = this->rt_field[RTF_DAYS].value;
        } else {
            etm.et_tm.tm_mday = 1;
        }
        etm.et_tm.tm_yday = -1;
        etm.et_tm.tm_min = this->rt_field[RTF_MINUTES].value;
        etm.et_tm.tm_sec = this->rt_field[RTF_SECONDS].value;

        auto epoch_secs = std::chrono::seconds(etm.to_timeval().tv_sec);
        retval
            = std::chrono::duration_cast<std::chrono::microseconds>(epoch_secs)
                  .count();
        retval += this->rt_field[RTF_MICROSECONDS].value;
    } else {
        retval = this->rt_field[RTF_YEARS].value * 12;
        retval = (retval + this->rt_field[RTF_MONTHS].value) * 30;
        retval = (retval + this->rt_field[RTF_DAYS].value) * 24;
        retval = (retval + this->rt_field[RTF_HOURS].value) * 60;
        retval = (retval + this->rt_field[RTF_MINUTES].value) * 60;
        retval = (retval + this->rt_field[RTF_SECONDS].value) * 1000 * 1000;
        retval = (retval + this->rt_field[RTF_MICROSECONDS].value);
    }

    return retval;
}
