/*
 * Copyright 2009- ECMWF.
 *
 * This software is licensed under the terms of the Apache Licence version 2.0
 * which can be obtained at http://www.apache.org/licenses/LICENSE-2.0.
 * In applying this licence, ECMWF does not waive the privileges and immunities
 * granted to it by virtue of its status as an intergovernmental organisation
 * nor does it submit to any jurisdiction.
 */

#include "ecflow/attribute/CronAttr.hpp"

#include <sstream>
#include <stdexcept>

#include <boost/date_time/posix_time/time_formatters.hpp> // requires boost date and time lib
#include <boost/token_functions.hpp>
#include <boost/tokenizer.hpp>

#include "ecflow/core/Calendar.hpp"
#include "ecflow/core/Converter.hpp"
#include "ecflow/core/Ecf.hpp"
#include "ecflow/core/Indentor.hpp"
#include "ecflow/core/PrintStyle.hpp"
#include "ecflow/core/Serialization.hpp"
#include "ecflow/core/Str.hpp"

using namespace std;
using namespace ecf;
using namespace boost;
using namespace boost::gregorian;
using namespace boost::posix_time;

// #define DEBUG_CRON_ATTR 1
// #define DEBUG_CRON_PARSING 1
// #define DEBUG_CRON_SIM 1

namespace ecf {

CronAttr::CronAttr() = default;

CronAttr::CronAttr(const std::string& str) {
    if (str.empty())
        throw std::runtime_error("CronAttr::CronAttr : empty string passed");
    std::vector<std::string> tokens;
    Str::split(str, tokens);
    if (tokens.empty())
        throw std::runtime_error("CronAttr::CronAttr : incorrect time string ?");

    size_t index = 0;
    timeSeries_  = TimeSeries::create(index, tokens, false /*parse_state*/);
}

void CronAttr::addWeekDays(const std::vector<int>& w) {
    weekDays_ = w;
    for (int day : weekDays_) {
        if (day < 0 || day > 6) {
            std::stringstream ss;
            ss << "Invalid range for day(" << day << ") of the week expected range is 0==Sun to 6==Sat";
            throw std::out_of_range(ss.str());
        }
        auto result = std::find(std::begin(last_week_days_of_month_), std::end(last_week_days_of_month_), day);
        if (result != std::end(last_week_days_of_month_)) {
            std::stringstream ss;
            ss << "Duplicate day(" << day << ") of the week also found in last week day of the month";
            throw std::runtime_error(ss.str());
        }
    }
}
void CronAttr::add_last_week_days_of_month(const std::vector<int>& w) {
    last_week_days_of_month_ = w;
    for (int day : last_week_days_of_month_) {
        if (day < 0 || day > 6) {
            std::stringstream ss;
            ss << "Invalid range for day(" << day << ") of the week expected range is 0==Sun to 6==Sat";
            throw std::out_of_range(ss.str());
        }
        auto result = std::find(std::begin(weekDays_), std::end(weekDays_), day);
        if (result != std::end(weekDays_)) {
            std::stringstream ss;
            ss << "Duplicate last week day (" << day << ") of the month also found in week day";
            throw std::runtime_error(ss.str());
        }
    }
}

void CronAttr::addDaysOfMonth(const std::vector<int>& d) {
    daysOfMonth_ = d;
    for (int day_of_month : daysOfMonth_) {
        if (day_of_month < 1 || day_of_month > 31) {
            std::stringstream ss;
            ss << "Invalid range for day of month(" << day_of_month << ") expected range is  1-31";
            throw std::out_of_range(ss.str());
        }
    }
}

void CronAttr::addMonths(const std::vector<int>& m) {
    months_ = m;
    for (int month : months_) {
        if (month < 1 || month > 12) {
            std::stringstream ss;
            ss << "Invalid range for month(" << month << ")  expected range is 1==Jan to 12==Dec";
            throw std::out_of_range(ss.str());
        }
    }
}

void CronAttr::print(std::string& os) const {
    Indentor in;
    Indentor::indent(os);
    write(os);
    if (!PrintStyle::defsStyle()) {
        timeSeries_.write_state(os, free_);
    }
    os += "\n";
}

std::string CronAttr::name() const {
    std::string ret;
    write(ret);
    timeSeries_.write_state_for_gui(ret, free_);
    return ret;
}

std::string CronAttr::toString() const {
    std::string ret;
    write(ret);
    return ret;
}

void CronAttr::write(std::string& ret) const {
    ret += "cron ";
    if (!weekDays_.empty()) {
        ret += "-w ";
        for (size_t i = 0; i < weekDays_.size(); ++i) {
            ret += ecf::convert_to<std::string>(weekDays_[i]);
            if (i != weekDays_.size() - 1)
                ret += ",";
        }
        if (last_week_days_of_month_.empty())
            ret += " ";
        else
            ret += ",";
    }
    if (!last_week_days_of_month_.empty()) {
        if (weekDays_.empty())
            ret += "-w ";
        for (size_t i = 0; i < last_week_days_of_month_.size(); ++i) {
            ret += ecf::convert_to<std::string>(last_week_days_of_month_[i]);
            ret += 'L';
            if (i != last_week_days_of_month_.size() - 1)
                ret += ",";
        }
        ret += " ";
    }

    if (!daysOfMonth_.empty()) {
        ret += "-d ";
        for (size_t i = 0; i < daysOfMonth_.size(); ++i) {
            ret += ecf::convert_to<std::string>(daysOfMonth_[i]);
            if (i != daysOfMonth_.size() - 1)
                ret += ",";
        }
        if (!last_day_of_month_)
            ret += " ";
    }
    if (last_day_of_month_) {
        if (daysOfMonth_.empty())
            ret += "-d L ";
        else
            ret += ",L ";
    }

    if (!months_.empty()) {
        ret += "-m ";
        for (size_t i = 0; i < months_.size(); ++i) {
            ret += ecf::convert_to<std::string>(months_[i]);
            if (i != months_.size() - 1)
                ret += ",";
        }
        ret += " ";
    }

    timeSeries_.write(ret); // no new line added, up to caller
}

std::string CronAttr::dump() const {
    std::stringstream ss;
    ss << toString();
    if (free_)
        ss << " (free)";
    else
        ss << " (holding)";
    return ss.str();
}

bool CronAttr::operator==(const CronAttr& rhs) const {
    if (free_ != rhs.free_) {
        return false;
    }
    if (last_day_of_month_ != rhs.last_day_of_month_)
        return false;
    if (weekDays_ != rhs.weekDays_)
        return false;
    if (last_week_days_of_month_ != rhs.last_week_days_of_month_)
        return false;
    if (daysOfMonth_ != rhs.daysOfMonth_)
        return false;
    if (months_ != rhs.months_)
        return false;
    return timeSeries_.operator==(rhs.timeSeries_);
}
bool CronAttr::structureEquals(const CronAttr& rhs) const {
    if (last_day_of_month_ != rhs.last_day_of_month_)
        return false;
    if (weekDays_ != rhs.weekDays_)
        return false;
    if (daysOfMonth_ != rhs.daysOfMonth_)
        return false;
    if (last_week_days_of_month_ != rhs.last_week_days_of_month_)
        return false;
    if (months_ != rhs.months_)
        return false;
    return timeSeries_.structureEquals(rhs.timeSeries_);
}

void CronAttr::calendarChanged(const ecf::Calendar& c) {
    // ensure this called first , since we need always update for relative duration ECFLOW-1648
    // This assumes that calendarChanged will set TimeSeries::isValid = true, at day change
    if (timeSeries_.calendarChanged(c)) {
        state_change_no_ = Ecf::incr_state_change_no();
    }

    if (free_) {
        return;
    }

    // Once a cron is free, it stays free until re-queue
    if (isFree(c)) {
        setFree();
    }
    // A cron is always re-queable, hence we use isFree to control when it can actually run.
}

void CronAttr::resetRelativeDuration() {
    if (timeSeries_.resetRelativeDuration()) {
        state_change_no_ = Ecf::incr_state_change_no();
    }
}

void CronAttr::setFree() {
    free_            = true;
    state_change_no_ = Ecf::incr_state_change_no();

#ifdef DEBUG_STATE_CHANGE_NO
    std::cout << "CronAttr::setFree()\n";
#endif
}

void CronAttr::clearFree() {
    free_            = false;
    state_change_no_ = Ecf::incr_state_change_no();

#ifdef DEBUG_STATE_CHANGE_NO
    std::cout << "CronAttr::clearFree()\n";
#endif
}

void CronAttr::miss_next_time_slot() {
    // A cron attribute with a single time slot is repeated indefinitely hence always re-queues
    timeSeries_.miss_next_time_slot();
    state_change_no_ = Ecf::incr_state_change_no();
}

// **************************************************************************************
// FOR DEBUG THIS IS THE MAIN FUNCTION, AS THIS DECIDES WHETHER WE CONTINE OR STOP
// **************************************************************************************
bool CronAttr::checkForRequeue(const ecf::Calendar& calendar) const {
    // checkForRequeue is called when a task/family has reach the complete state
    // This simple checks if node should be put in re-queued state
    // A cron is always re-queable

    // Hence: In order to use this it should be used in conjunction with a
    // with a parent node that has complete  expression, (& maybe a dummy task)
    // This will allow its use with a parent repeat somewhere in the hierarchy
    return true;
}

bool CronAttr::validForHybrid(const ecf::Calendar& calendar) const {
    if (timeSeries_.hasIncrement()) {
        if (last_day_of_month_)
            return false; // relies on day change
        if (!months_.empty())
            return false; // relies on day change
        if (!daysOfMonth_.empty())
            return false; // relies on day change
        if (!weekDays_.empty()) {
            if (weekDays_.size() != 1)
                return false; // relies on day change
            return (weekDays_[0] == calendar.day_of_week());
        }

        // cron 10:00 20:00 01:00  // valid for hybrid ?
        return true;
    }

    // A time series that does not have an increment runs indefinitely and hence relies on day change
    // cron 23:00
    return false;
}

bool CronAttr::why(const ecf::Calendar& c, std::string& theReasonWhy) const {
    // This will logically AND all the times
    if (isFree(c))
        return false;

    // We are here because:
    //  1/ Not on a valid time slot in the time series
    //  *OR*
    //  2/ Logical *AND* of day of week, day of month, or month returned false
    theReasonWhy += "is cron dependent";

    // Lets say that the time series was NOT free.
    // First check if week day, day of month, month, matches
    if (is_day_of_week_day_of_month_and_month_free(c)) {

        if (timeSeries_.is_valid()) {

            // This can apply to single and series
            boost::posix_time::time_duration calendar_time = timeSeries_.duration(c);
            if (calendar_time < timeSeries_.start().duration()) {
                timeSeries_.why(c, theReasonWhy);
                return true;
            }

            // calendar_time >= timeSeries_.start().duration()
            if (timeSeries_.hasIncrement()) {
                if (calendar_time < timeSeries_.finish().duration()) {
                    timeSeries_.why(c, theReasonWhy);
                    return true;
                }
            }
        }
        // calendar_time >= timeSeries_.start().duration() && calendar_time >= timeSeries_.finish().duration()
        // past the end of time slot, find next valid date
    }

    // take into account, user can use run/force complete to miss time slots
    bool do_a_requeue = timeSeries_.requeueable(c);
    if (do_a_requeue && weekDays_.empty() && daysOfMonth_.empty() && months_.empty()) {
        TimeSlot the_next_time_slot = timeSeries_.compute_next_time_slot(c);
        if (the_next_time_slot.isNULL()) {
            theReasonWhy += " ( *re-queue* to run at this time ";
        }
        else {
            theReasonWhy += " ( *re-queue* to run at ";
            theReasonWhy += the_next_time_slot.toString();
        }

        theReasonWhy += ", otherwise next run is at ";
    }
    else {
        theReasonWhy += " ( next run is at ";
    }

    // Find the *NEXT* date that matches, and use the first time slot
    boost::gregorian::date the_next_date = next_date(c);
    theReasonWhy += timeSeries_.start().toString();
    theReasonWhy += " ";
    theReasonWhy += to_simple_string(the_next_date);

    std::stringstream ss;
    TimeSlot currentTime = TimeSlot(timeSeries_.duration(c));
    ss << ", current time ";
    if (timeSeries_.relative())
        ss << "+";
    ss << currentTime.toString() << " " << to_simple_string(c.date()) << " )";
    theReasonWhy += ss.str();
    return true;
}

void CronAttr::reset_only() {
    clearFree();
    timeSeries_.reset_only();
}

void CronAttr::reset(const ecf::Calendar& c) {
    clearFree();
    timeSeries_.reset(c);
}

void CronAttr::requeue(const ecf::Calendar& c, bool reset_next_time_slot) {
    clearFree();
    timeSeries_.requeue(c, reset_next_time_slot);
}

bool CronAttr::isFree(const ecf::Calendar& c) const {
    // The FreeDepCmd can be used to free the crons,
    if (free_) {
        return true;
    }

    if (!timeSeries_.isFree(c))
        return false;

    // Ok time series is Free

    // ********************************************************************
    // IMPORTANT: when we have multiple week days, days of month and months
    // Then we are *ONLY* free, if *ALL* are free, i.e we need AND behaviour
    // ********************************************************************
    return is_day_of_week_day_of_month_and_month_free(c);
}

bool CronAttr::is_day_of_week_day_of_month_and_month_free(const ecf::Calendar& c) const {
#ifdef DEBUG_CRON_SIM
    cout << toString() << "  cal : " << to_simple_string(c.date()) << " c.day_of_week:" << c.day_of_week()
         << " c.day_of_month:" << c.day_of_month();
    cout.flush();
#endif

    bool the_week_day_matches =
        weekDays_.empty() && last_week_days_of_month_.empty(); // week day matches if no week days
    bool the_day_of_month_matches = daysOfMonth_.empty();      // day of month if no days of month
    bool the_month_matches        = months_.empty();           // month matches if no months

    if (!weekDays_.empty())
        the_week_day_matches = week_day_matches(c.day_of_week());
    if (!the_week_day_matches && !last_week_days_of_month_.empty())
        the_week_day_matches = last_week_day_of_month_matches(c);

    if (!daysOfMonth_.empty() || last_day_of_month_)
        the_day_of_month_matches = day_of_month_matches(c.day_of_month(), c);
    if (!months_.empty())
        the_month_matches = month_matches(c.month());

    // Remember we *AND* across -w, -d, -m or *OR* for each element in -w, -d,-m
    bool matches = false;
    if (daysOfMonth_.empty() && !last_day_of_month_ && months_.empty()) {
        // cron -w 0L 10:00    # run on the last sunday of each month
        matches = the_week_day_matches; // only week day,
    }
    else {
        matches = the_week_day_matches && the_day_of_month_matches && the_month_matches;
    }

#ifdef DEBUG_CRON_SIM
    if (matches) {
        cout << " *MATCHES*";
    }
    cout << "\n";
#endif

    return matches;
}

bool CronAttr::week_day_matches(int theDayOfWeek) const {
    for (int theWeekDay : weekDays_) {
        if (theDayOfWeek == theWeekDay)
            return true;
    }
    return false;
}

bool CronAttr::last_week_day_of_month_matches(const ecf::Calendar& c) const {
    int cal_day_of_week                                                     = c.day_of_week();
    boost::gregorian::date last_day_of_month                                = c.date().end_of_month();
    boost::gregorian::date_duration diff_current_date_and_last_day_of_month = last_day_of_month - c.date();

    for (int cron_last_week_day_of_month : last_week_days_of_month_) {
        if (cal_day_of_week == cron_last_week_day_of_month) {

            if (diff_current_date_and_last_day_of_month.days() < 7) {
                return true;
            }
        }
    }
    return false;
}

bool CronAttr::day_of_month_matches(int theDayOfMonth, const ecf::Calendar& c) const {
    for (int dayOfMonth : daysOfMonth_) {
        if (theDayOfMonth == dayOfMonth)
            return true;
    }
    if (last_day_of_month_) {
        return c.date() == c.date().end_of_month();
    }
    return false;
}

bool CronAttr::month_matches(int theMonth) const {
    for (int month : months_) {
        if (theMonth == month)
            return true;
    }
    return false;
}

//------------------------------------------------------------------

bool CronAttr::checkInvariants(std::string& errormsg) const {
    return timeSeries_.checkInvariants(errormsg);
}

//--------------------------------------------------------------

boost::gregorian::date CronAttr::next_date(const ecf::Calendar& calendar) const {
    // Find the next date that matches, day of week, day of year, and month
    // that is greater than todays date. This *ASSUMES* day of week, day of month,
    // and month is *ANDED* together
    boost::gregorian::date_duration one_day(1);
    boost::gregorian::date future_date = calendar.date(); // todays date

#ifdef DEBUG_CRON_SIM
    cout << "cron : " << toString() << "\n";
    cout << "future_date start : " << to_simple_string(future_date) << "\n";
#endif
    future_date += one_day; // add one day, so its in the future

    while (true) {

        bool week_day_matches                   = weekDays_.empty();                // week day matches if no week days
        bool the_last_week_day_of_month_matches = last_week_days_of_month_.empty(); // matches if EMPTY
        bool day_of_month_matches               = daysOfMonth_.empty();             // day of month if no days of month
        bool month_matches                      = months_.empty();                  // month matches if no months
        if (daysOfMonth_.empty() && last_day_of_month_)
            day_of_month_matches = false;

        // deal with case where we have: cron -w 0,1
        for (int weekDay : weekDays_) {
            if (future_date.day_of_week().as_number() == weekDay) {
                week_day_matches = true;
                break;
            }
        }
        // *IMPORTANT* the days in weekDays_ and last_week_days_of_month_ can *NOT* overlap.
        for (int weekDay : last_week_days_of_month_) {
            if (future_date.day_of_week().as_number() == weekDay) {
                boost::gregorian::date_duration diff = future_date - future_date.end_of_month();
                if (diff.days() < 7) {
                    the_last_week_day_of_month_matches = true;
                }
                break;
            }
        }

        // deal with case where we have: cron  -d 14,15,16,L  # L means last day of month
        if (!daysOfMonth_.empty() || last_day_of_month_) {
            for (int d : daysOfMonth_) {
                if (future_date.day() == d) {
                    day_of_month_matches = true;
                    break;
                }
            }
            if (last_day_of_month_ && future_date == future_date.end_of_month()) {
                day_of_month_matches = true;
            }
        }

        // deal with case where we have: cron -w 0,1 -d 14,15,16 -m 8, 9
        for (int month : months_) {
            if (future_date.month() == month) {
                month_matches = true;
                break;
            }
        }

        // if it all matches, then return the future day
        // Remember we *AND* across -w, -d, -m or *OR* for each element in -w, -d,-m
        if ((week_day_matches || the_last_week_day_of_month_matches) && day_of_month_matches && month_matches) {
            break; // return future_date, replaced with break to  keep HPUX compiler happy
                   // otherwise it complains that return at the end of the function is
                   // unreachable
        }

        future_date += one_day;
#ifdef DEBUG_CRON_SIM
        cout << "future_date " << to_simple_string(future_date) << "\n";
#endif
    }
    return future_date; // should never happen, i.e we can find future date that matches
}

//=========================================================================================================
// code for parsing a cron:

static bool isComment(const std::string& token) {
    if (token.find("#") == std::string::npos)
        return false;
    return true;
}

static bool isTimeSpec(const std::string& token) {
    if (token.find(Str::COLON()) == std::string::npos)
        return false;
    return true;
}

static bool isOption(const std::string& token) {
    if (token.find("-w") != std::string::npos)
        return true;
    if (token.find("-d") != std::string::npos)
        return true;
    if (token.find("-m") != std::string::npos)
        return true;
    return false;
}

static std::string nextToken(size_t& index, const std::vector<std::string>& lineTokens) {
    assert(index < lineTokens.size());
    index++;
    if (index < lineTokens.size()) {
#ifdef DEBUG_CRON_PARSING
        cerr << "nextToken lineTokens[" << index << "] = " << lineTokens[index] << "\n";
#endif
        return lineTokens[index];
    }
#ifdef DEBUG_CRON_PARSING
    cerr << "nextToken empty \n";
#endif
    return string();
}

std::string extract_list(size_t& index, const std::vector<std::string>& lineTokens) {
    // cron -w 0,1L,2L,3 -d 1,12,14,L   -m 5,6,7,8   10:00 20:00 01:00
    assert(index < lineTokens.size());

    // Collate the list of integers, these may have been separated by spaces
    // since we stop on option or time spec, the top level code should decrement index
    std::string theIntList;
    while (index < lineTokens.size() && (!isOption(lineTokens[index]) || !isTimeSpec(lineTokens[index]))) {
        string theNextToken = nextToken(index, lineTokens);
        if (theNextToken.empty())
            break;
        if (isOption(theNextToken))
            break;
        if (isTimeSpec(theNextToken))
            break;
        theIntList += theNextToken;
    }
#ifdef DEBUG_CRON_PARSING
    cerr << "theIntList = " << theIntList << "\n";
#endif
    return theIntList;
}

std::vector<int> extract_month(size_t& index, const std::vector<std::string>& lineTokens, const std::string& option) {
    // cron -w 0,1L,2L,3 -d 1,12,14,L   -m 5,6,7,8   10:00 20:00 01:00
    assert(index < lineTokens.size());

    // Collate the list of integers, these may have been separated by spaces
    // since we stop on option or time spec, the top level code should decrement index
    std::string theIntList = extract_list(index, lineTokens);

    // should have 0,1,2,3
    std::vector<int> theIntVec;
    char_separator<char> sep(",", nullptr, boost::drop_empty_tokens);
    typedef boost::tokenizer<boost::char_separator<char>> tokenizer;
    tokenizer theTokenizer(theIntList, sep);

    for (tokenizer::iterator beg = theTokenizer.begin(); beg != theTokenizer.end(); ++beg) {
        string theIntToken = *beg;
        ecf::algorithm::trim(theIntToken);
        if (theIntToken.empty())
            continue;

        try {
            auto theInt = ecf::convert_to<int>(theIntToken);
            theIntVec.push_back(theInt);
        }
        catch (const ecf::bad_conversion&) {
            std::stringstream ss;
            ss << "Invalid cron option: " << option;
            throw std::runtime_error(ss.str());
        }
    }
    return theIntVec;
}

void extract_days_of_week(size_t& index,
                          const std::vector<std::string>& lineTokens,
                          const std::string& option,
                          std::vector<int>& days_of_week,
                          std::vector<int>& last_week_days_of_month) {
    // cron -w 0,1L,2L,3 10:00 20:00 01:00
    assert(index < lineTokens.size());

    // Collate the list of integers, these may have been separated by spaces
    // since we stop on option or time spec, the top level code should decrement index
    std::string theIntList = extract_list(index, lineTokens);

    // should have 0,1,2,3,4L
    char_separator<char> sep(",", nullptr, boost::drop_empty_tokens);
    typedef boost::tokenizer<boost::char_separator<char>> tokenizer;
    tokenizer theTokenizer(theIntList, sep);

    for (tokenizer::iterator beg = theTokenizer.begin(); beg != theTokenizer.end(); ++beg) {
        string theIntToken = *beg;
        ecf::algorithm::trim(theIntToken);
        if (theIntToken.empty())
            continue;

        try {
            if (theIntToken.size() == 2) {
                if (theIntToken[1] != 'L') {
                    std::stringstream ss;
                    ss << "Invalid cron option: " << option << " " << theIntToken;
                    throw std::runtime_error(ss.str());
                }
                auto theInt = ecf::convert_to<int>(theIntToken[0]);
                last_week_days_of_month.push_back(theInt);
            }
            else {
                auto theInt = ecf::convert_to<int>(theIntToken);
                days_of_week.push_back(theInt);
            }
        }
        catch (const ecf::bad_conversion&) {
            std::stringstream ss;
            ss << "Invalid cron option: " << option;
            throw std::runtime_error(ss.str());
        }
    }
}

void extract_days_of_month(size_t& index,
                           const std::vector<std::string>& lineTokens,
                           const std::string& option,
                           std::vector<int>& days_of_month,
                           bool& last_day_of_month) {
    // cron -d 1,12,14,L 10:00 20:00 01:00

    assert(index < lineTokens.size());

    // Collate the list of integers, these may have been separated by spaces
    // since we stop on option or time spec, the top level code should decrement index
    std::string theIntList = extract_list(index, lineTokens);

    // should have 0,1,2,3,4L
    char_separator<char> sep(",", nullptr, boost::drop_empty_tokens);
    typedef boost::tokenizer<boost::char_separator<char>> tokenizer;
    tokenizer theTokenizer(theIntList, sep);

    for (tokenizer::iterator beg = theTokenizer.begin(); beg != theTokenizer.end(); ++beg) {
        string theIntToken = *beg;
        ecf::algorithm::trim(theIntToken);
        if (theIntToken.empty())
            continue;

        try {
            if (theIntToken == "L")
                last_day_of_month = true;
            else {
                auto theInt = ecf::convert_to<int>(theIntToken);
                days_of_month.push_back(theInt);
            }
        }
        catch (const ecf::bad_conversion&) {
            std::stringstream ss;
            ss << "Invalid cron option: " << option;
            throw std::runtime_error(ss.str());
        }
    }
}

void extractOption(CronAttr& cronAttr, size_t& index, const std::vector<std::string>& lineTokens) {
    assert(index < lineTokens.size());
    if (lineTokens[index] == "-w") {
        std::vector<int> days_of_week;
        std::vector<int> last_week_days_of_month;
        extract_days_of_week(index, lineTokens, "week days", days_of_week, last_week_days_of_month);

        cronAttr.addWeekDays(days_of_week);
        cronAttr.add_last_week_days_of_month(last_week_days_of_month);
    }
    else if (lineTokens[index] == "-d") {
        std::vector<int> days_of_month;
        bool last_day_of_month = false;
        extract_days_of_month(index, lineTokens, "Days of the month", days_of_month, last_day_of_month);

        cronAttr.addDaysOfMonth(days_of_month);
        if (last_day_of_month)
            cronAttr.add_last_day_of_month();
    }
    else if (lineTokens[index] == "-m") {
        cronAttr.addMonths(extract_month(index, lineTokens, "Months"));
    }
    else
        throw std::runtime_error("extractOption: Invalid cron option :" + lineTokens[index]);
}

void CronAttr::parse(CronAttr& cronAttr, const std::vector<std::string>& lineTokens, size_t index, bool parse_state) {
    // cron 23:00                 # run every day at 23:00
    // cron 10:00 20:00 01:00     # run every hour between 10am and 8pm
    // cron -w 0,1 10:00          # run every sunday and monday at 10am
    // cron -d 10,11,12 12:00     # run 10th, 11th and 12th of each month at noon
    // cron -m 1,2,3 12:00        # run on Jan,Feb and March every day at noon.
    // cron -w 0 -m 5,6,7,8 10:00 20:00 01:00 # run every sunday, between May-Aug, every hour between 10am and 8pm
    // cron -w 0,1,2L -d 5,6,L  23:00 # run every sunday,monday, and last tuesday of the month, 5,6 on month, and *last*
    // day of month @11 pm

    // make *sure* a time spec is specified
    bool time_spec_specified = false;
    size_t line_tokens_size  = lineTokens.size();
    while (index < line_tokens_size) {

        const std::string& token = lineTokens[index];
#ifdef DEBUG_CRON_PARSING
        cerr << "CronAttr::doParse " << token << "\n";
#endif

        if (isOption(token)) {
#ifdef DEBUG_CRON_PARSING
            cerr << "CronAttr::doParse isOption \n";
#endif
            extractOption(cronAttr, index, lineTokens);
            index--; // since we did a look ahead
        }
        else if (!time_spec_specified && isTimeSpec(token)) {
#ifdef DEBUG_CRON_PARSING
            cerr << "CronAttr::doParse isTimeSpec \n";
#endif
            // index is passed by *reference*, and used skip over time series
            cronAttr.addTimeSeries(TimeSeries::create(index, lineTokens, parse_state));
            time_spec_specified = true;
            if (parse_state) {
                //  if index is on the comment, back track, so that we can add cron state( free)
                if (index < line_tokens_size && lineTokens[index] == "#") {
                    index--;
                }
            }
            else
                break; // need to read state after comment
        }
        else if (isComment(token)) {
            // cron -m 1,2,3 12:00        # free
            if (parse_state && index + 1 < line_tokens_size) {
                if (lineTokens[index + 1] == "free") {
                    cronAttr.setFree();
                }
            }
            break;
        }
        index++;
    }

    if (!time_spec_specified) {
        throw std::runtime_error("Invalid cron, no time specified");
    }

#ifdef DEBUG_CRON_PARSING
    cronAttr.print(cerr);
    cerr << "\n";
#endif
}

CronAttr CronAttr::create(const std::string& cronString) {
    std::vector<std::string> lineTokens;
    Str::split(cronString, lineTokens);

    CronAttr theCronAttr;
    if (lineTokens.empty()) {
        return theCronAttr;
    }

    // adjust the index
    size_t index = 0;
    if (lineTokens[0] == "cron") {
        index = 1;
    }

    parse(theCronAttr, lineTokens, index);
    return theCronAttr;
}

template <class Archive>
void CronAttr::serialize(Archive& ar, std::uint32_t const version) {
    ar(CEREAL_NVP(timeSeries_));
    CEREAL_OPTIONAL_NVP(ar, weekDays_, [this]() { return !weekDays_.empty(); }); // conditionally save
    CEREAL_OPTIONAL_NVP(
        ar, last_week_days_of_month_, [this]() { return !last_week_days_of_month_.empty(); }); // conditionally save
    CEREAL_OPTIONAL_NVP(ar, daysOfMonth_, [this]() { return !daysOfMonth_.empty(); });         // conditionally save
    CEREAL_OPTIONAL_NVP(ar, months_, [this]() { return !months_.empty(); });                   // conditionally save
    CEREAL_OPTIONAL_NVP(ar, free_, [this]() { return free_; });                                // conditionally save
    CEREAL_OPTIONAL_NVP(ar, last_day_of_month_, [this]() { return last_day_of_month_; });      // conditionally save
    CEREAL_OPTIONAL_NVP(ar, w_, [this]() { return w_ != 0; });                                 // conditionally save
}
CEREAL_TEMPLATE_SPECIALIZE_V(CronAttr);

} // namespace ecf
