/*
 * 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.
 */

#ifndef ecflow_core_TimeSeries_HPP
#define ecflow_core_TimeSeries_HPP

///
/// \brief Represents a set of time instants (possibly just one)
///

#include <cstdint>
#include <vector>

#include <boost/date_time/posix_time/posix_time.hpp>

#include "ecflow/core/TimeSlot.hpp"

namespace ecf {
class Calendar;
} // namespace ecf

namespace ecf {
/// TimeSeries can have a single time slot or a series of time slots
///
///
/// We need nextTimeSlot_ data member to record the next valid time slot, otherwise when
/// we have a time series, all times within the time series would be valid for
/// Job submission. We do not know how long a job will run, hence when incrementing
/// the nextTimeSlot_, we need the *NEXT* available time slot *after* the current calendar time.
///
/// Usage pattern is :
///           TimeSeries.calendarChanged()   // Called every minute. Calls isFree(). Once free we stay free, until
///           requeue.
///                                          // At midnight clear time expiration and reset next time slot
///           TimeSeries.isFree(..)          // called when node is QUEUED:
///                                          // during dependency evaluation, checks next time slot, against calendar
///                                          time
///                                          // Once free a node stays free, until requeue()
///           TimeSeries.checkForRequeue();  // called when node is COMPLETE: Checks if next available time slot is
///           valid
///                                          // *Must* return false for last time slot
///                                          // ECFLOW-130 jobs that start before midnight and finish after midnight
///                                          should not requeue
///           TimeSeries.requeue();          // called when node is re-queued.
///                                          // Sets value to next valid time slot after calendar time.
///                                          // Can *expire* the time, relies on TimeSeries.calendarChanged() to clear
///                                          time expiration
///                                          // after midnight
/// ECFLOW-130 jobs that start before midnight and finish after midnight should not requeue

// Use compiler, destructor, copy constructor and assignment operator
class TimeSeries {
public:
    TimeSeries();
    TimeSeries(int hour, int minute, bool relative = false);
    explicit TimeSeries(const TimeSlot&, bool relative = false);
    TimeSeries(const TimeSlot& start, const TimeSlot& finish, const TimeSlot& incr, bool relative = false);

    bool operator<(const TimeSeries& rhs) const;

    // returns true if a state change is made
    // Will clear time expiration flag at midnight
    bool calendarChanged(const ecf::Calendar& c);

    // relative duration stored locally since it can be reset, when used with repeats
    // returns true if relative duration is reset/ i.e state change has been made
    bool resetRelativeDuration();
    void reset_only();
    void reset(const ecf::Calendar& c);

    // Increment time series. Will find the next time slot after current calendar
    // record the current suite time, to check of jobs finish after midnight
    void requeue(const ecf::Calendar& c, bool reset_next_time_slot = true);

    // Since we can miss next time slot , allow its computation for the
    // use with why command, returns a NULL timeslot if next time is invalid
    // This functionality will mirror the requeue(..) function
    TimeSlot compute_next_time_slot(const ecf::Calendar& c) const;

    // Return true calendar is before or within scheduled time
    bool requeueable(const ecf::Calendar& c) const;

    /// if relativeToSuiteStart returns the relative duration, else returns calendar suite time of day.
    /// The returned resolution is in minutes
    boost::posix_time::time_duration duration(const ecf::Calendar& calendar) const;

    /// If time has expired, returns false
    /// Note: relative means relative to suite start, or relative to the beginning of repeated node.
    /// Is of the form hh:mm.   This means relative has a range 00:00->99.59
    /// For a single slot time series, should not be re queued again, and hence
    /// should fail checkForRequeue. Likewise when the the current value is
    /// the finish, then node should also not be re queued, otherwise the
    /// node will be stuck in queued state.
    bool isFree(const ecf::Calendar& calendar) const;

    /// For single slot time based attributes we need additional context (i.e the_min,the_max parameter)
    /// in order to determine whether we should re-queue.
    /// Additionally when we have a time range, what if the last jobs runs over midnight. In this case
    /// we need to return false. i.e do not re-queue ECFLOW-130
    bool checkForRequeue(const ecf::Calendar& calendar,
                         const TimeSlot& the_min,
                         const TimeSlot& the_max,
                         bool cmd_context = false) const;

    void min_max_time_slots(TimeSlot& the_min, TimeSlot& the_max) const;

    // Called when explicitly Free holding time dependency. via FreeDepCmd
    // We want to avoid the next time slot.
    void miss_next_time_slot();

    void why(const ecf::Calendar&, std::string& theReasonWhy) const;

    bool hasIncrement() const { return !finish_.isNULL(); }
    const TimeSlot& start() const { return start_; }
    const TimeSlot& finish() const { return finish_; }
    const TimeSlot& incr() const { return incr_; }
    const TimeSlot& value() const { return nextTimeSlot_; }
    bool relative() const { return relativeToSuiteStart_; }
    void free_slots(std::vector<boost::posix_time::time_duration>&) const;

    void print(std::string&) const;
    bool operator==(const TimeSeries& rhs) const;
    bool operator!=(const TimeSeries& rhs) const { return !operator==(rhs); }
    bool structureEquals(const TimeSeries& rhs) const;

    /// returns true if no time specified
    bool isNULL() const { return start_.isNULL(); }

    std::string state_to_string(bool isFree) const;
    void write_state(std::string&, bool free) const;
    void write_state_for_gui(std::string&, bool free) const;
    std::string toString() const;
    void write(std::string&) const;
    std::string dump() const;
    bool checkInvariants(std::string& errormsg) const;

    /// expects HH:MM or +HH:MM will throw std:runtime_error for errors,
    /// *if* hour not in range(0-24), minutes(0-59), *and* check_time parameter is enabled
    static bool getTime(const std::string& time, int& hour, int& min, bool check_time = true);

    /// extract string like
    ///     time +00:00 20:00 00:10 # this is a comment which will be ignored. index = 1
    ///     time +20:00        // index = 1
    ///     today 20:00        // index = 1
    ///     +00:00 20:00 00:10 // index = 0
    ///     +20:00             // index = 0
    /// will throw std:runtime_error for errors
    /// will assert if index >= lineTokens.size()
    static ecf::TimeSeries create(size_t& index, // where we should start
                                  const std::vector<std::string>& lineTokens,
                                  bool read_state = false);

    // like above but string should not contain "time"
    static ecf::TimeSeries create(const std::string& str);

    // Parse state associated with the time series: Ignores attributes like free.
    static void parse_state(size_t index, const std::vector<std::string>& lineTokens, ecf::TimeSeries& ts);
    void set_isValid(bool b) { isValid_ = b; }                          // for test only
    void set_next_time_slot(const TimeSlot& ts) { nextTimeSlot_ = ts; } // needed for test only
    const TimeSlot& get_next_time_slot() const { return nextTimeSlot_; }

    // Is the time still valid, return false means time has expired.
    bool is_valid() const { return isValid_; }

private:
    static void testTime(int hour, int minute);

    // HANDLE CASE WHERE FINISH MINUTES IS NOT DIVISIBLE BY THE INCREMENT
    // time 00:00 23:59 00:10  last valid time is 23:50
    // time 00:30 23:59 04:00  last valid time is 20:30
    // The last valid time here is 23:50/20:30 must return false, to stop node being
    // stuck in queued mode In the case where the FINISH is NOT a multiple of the increment
    // Need a different mechanism to determine the end.
    void compute_last_time_slot(); // only call for series

    bool match_duration_with_time_series(const boost::posix_time::time_duration& relative_or_real_td) const;

private:
    TimeSlot start_;
    TimeSlot finish_;
    TimeSlot incr_;
    TimeSlot nextTimeSlot_;             // nextTimeSlot_ >= start && is incremented by incr
    mutable TimeSlot suiteTimeAtReque_; // NOT persisted, check of day change, between requeue -> checkForRequeue, when
                                        // we have series
    boost::posix_time::time_duration relativeDuration_;
    boost::posix_time::time_duration lastTimeSlot_; // Only used when we have a series, can be generated
    bool relativeToSuiteStart_{false};
    bool isValid_{true}; // Needed for single slot to avoid multiple jobs submissions

private:
    // Note: isValid_      is persisted for use by why() command on the client side.
    // Note: nextTimeSlot_ is persisted for use by why() command on the client side.
    // Note: relativeDuration_ is persisted for use by why() command on the client side.
    friend class cereal::access;
    template <class Archive>
    void serialize(Archive& ar, std::uint32_t const /*version*/);
};

std::ostream& operator<<(std::ostream& os, const TimeSeries*);
std::ostream& operator<<(std::ostream& os, const TimeSeries&);

} // namespace ecf

#endif /* ecflow_core_TimeSeries_HPP */
