/*
 * 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_Calendar_HPP
#define ecflow_core_Calendar_HPP

///
/// \brief The calendar encapsulates date and time. The date is derived from the time.
///        The calendar object is initialised when the suite begins.
///
/// After each update the calendar will store the time_duration between the init() function
/// call and the update() function call.
/// The calendar is to be used for all reference to time and date
/// This will stop different time functionality from getting out of step.
///
/// Examples of Use of calendar are:
///   o Generated time variables
///   o Time dependent attributes: TimeAttr,TodaySeries,CronAttr,DateAttr,DayAttr
///     ****************************************************************************
///     ** A time attribute can have a +, which means time relative to suite start
///     ** This is stored on the TimeSeries, as it isNode/Attribute specific.
///     ** i.e repeated families will have its own relative start time.
///     *****************************************************************************
///
/// DESIGN CONSIDERATIONS:
///   Real and Hybrid:
///   Real
///     calendar is like a normal calendar where time and date are related
///     and day/date changes at midnight.
///   Hybrid:
///     There is currently confusion about how this is supposed to work.
///     The date is not supposed to change. (According to John date updates at suite restart?)
///     This has important implications, i.e does the day change ?
///     If the day does not change, then many of suites will never complete.
///       i.e if we use repeat, with a single time series,  "time 10:00"
///     *** This relies on a day change to reset time attribute at midnight. ****
///  Conclusion: Will support day change for both REAL and HYBRID (date does not change)
///
/// Calendar Updates:
///   How and when should we update the calendar?
///     In both the approaches below we need to make a distinction/separation
///     between the server poll and calendar update. This is required for testing
///
///   o Poll/Job submission interval in server is used to update calendar .
///     +: No time slots will be missed. even if server is suspended/restarted.
///        since suspending the server, also suspends the calendar updates
///     +: Suite relative times will continue to work even after server stopped/started
///     +: Avoids additional system call.
///     +: Lead's to more deterministic behaviour
///     -: If server is suspended and restarted the calendar will NOT be in
///        phase with system clock. (Its not clear to me why this should be an issue?)
///     ?: If the server is run for several days is there a possibility for the poll
///        update to get out of skew with real time. This is only possible if
///        job dependencies take more the 60 seconds to resolve.
///      *** THIS FUNCTIONALITY NEEDS TO BE ADVERTISED, SO THAT USERS ARE AWARE OF IT
///      *** THIS FUNCTIONALITY IS AVAILABLE VIA -s flag on the clock attribute .i.e
///      *** the -s stand's for stop start clock in line with the server
///            clock real 20.1.2007 +01:00 -s
///            clock hybrid -s
///
///   o Poll/Job submission interval in server is used Initiate an update of calendar via a system call.
///     +: calendar is always in phase with system clock.
///        Many task job dependencies depend on ordering based on real time.
///     -: Requires additional system call for each poll in the server
///     -: Time slots can be missed. (i.e if server suspended/restarted).
///        There is no catch up. (?? See TimeDependencies.ddoc)
///     -: Relative times will not be adhered too, when server stopped/started.
///     -: Will require more manual intervention ?
///
///  Conclusion:: The clock attribute will be changed to add both capabilities
///
///  Resolution: Will support 1 minute resolution
///

#include <cstdint>

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

namespace cereal {
class access;
}

namespace ecf {

class CalendarUpdateParams; // forward declare

class Calendar {
public:
    enum Clock_t {
        REAL,  // like a normal clock
        HYBRID // date does not change, but will support day change. See Above.
    };

    /// Defaults to the REAL clock
    Calendar();
    Calendar(const Calendar&);
    Calendar& operator=(const Calendar&);
    bool operator==(const Calendar&) const;

    /// Initialise the Calendar.
    void init(Clock_t clock);

    // for test init and begin calendar
    void init(const boost::posix_time::ptime& time, Clock_t clock = Calendar::REAL);

    /// Start the Calendar.  Parameter time can include gain.
    void begin(const boost::posix_time::ptime&);

    /// Update the calendar using the input. Will Store internally the time duration
    /// between the init() function call and the last update.
    /// The for_test parameter is *ONLY* used if we are using real time calendar
    void update(const ecf::CalendarUpdateParams&);

    /// This is used to improve resolution of state change time. ECFLOW-1512
    void update_duration_only(boost::posix_time::ptime& time_now);

    // Used for test only, will call the function above
    void update(const boost::posix_time::time_duration&);
    void update(const boost::posix_time::ptime& time_now);

    // The following were added as a performance optimisation
    // Represent a day within a week (range 0==Sun to 6==Sat)
    int day_of_week() const;  // same as suiteTime().date().day_of_week().as_number()
    int day_of_year() const;  // same as suiteTime().date().day_of_year()
    int day_of_month() const; // same as suiteTime().date().day()
    int month() const;        // same as suiteTime().date().month()
    int year() const;         // same as suiteTime().date().year()

    /// returns true if the day changed, this will update for REAL and HYBRID .
    bool dayChanged() const { return dayChanged_; }

    /// returns the last calendar increment
    const boost::posix_time::time_duration& calendarIncrement() const { return increment_; }

    /// return the init() time + the accumulated duration from calls to update(...)
    /// This should only be used when this calendar is real.
    const boost::posix_time::ptime& suiteTime() const { return suiteTime_; }
    std::string suite_time_str() const;

    // duration since last call to init, essentially suite duration
    const boost::posix_time::time_duration& duration() const { return duration_; }

    /// return real time, when the calendar was begun/initialised.
    /// This is used to update the duration_, which is recorded for each state change in the node
    /// Hence to when we can compute when a state change occurred by using:
    ///   boost::posix_time::ptime time_of_state_change = begin_time() + node->get_state().second(duration)
    const boost::posix_time::ptime& begin_time() const { return initLocalTime_; }

    /// return the date, for real calendar this corresponds to the date on suiteTime_
    /// for hybrid,  the date does not change, and hence we return date for initTime_
    boost::gregorian::date date() const;

    /// The calendar type. For hybrid clocks the date does not update.
    bool hybrid() const { return (ctype_ == Calendar::HYBRID) ? true : false; }

    /// for debug,  must link with boost date and time library
    void dump(const std::string& title) const;

    /// for debug,  must link with boost date and time library
    std::string toString() const;

    void write_state(std::string&) const;
    void read_state(const std::string& line, const std::vector<std::string>& lineTokens);

    bool checkInvariants(std::string& errorMsg) const;

    // allow Suite memento to update calendar type
    void set_clock_type(Clock_t ct) { ctype_ = ct; }

    /// Will return either second_clock::universal_time()/UTC ( the other alternative is second_clock::local_time() )
    /// This is because boost deadline timer is based on UTC clock
    /// >>> The deadline_timer typedef is based on a UTC clock, and all operations (expires_at, expires_from_now) work
    /// in UTC time.
    /// >>> If you want it to use a different clock (such as the local time clock), you can use basic_deadline_timer<>
    /// with your own traits class.
    /// >>> Please see the "Timers" example.
    /// Taken from boost date/time doc
    /// If you want exact agreement with wall-clock time, you must use either UTC or local time. If you compute a
    /// duration by subtracting one UTC time from another and you want an answer accurate to the second, the two times
    /// must not be too far in the future because leap seconds affect the count but are only determined about 6 months
    /// in advance. With local times a future duration calculation could be off by an entire hour, since legislatures
    /// can and do change DST rules at will. If you want to handle wall-clock times in the future, you won't be able (in
    /// the general case) to calculate exact durations, for the same reasons described above. If you want accurate
    /// calculations with future times, you will have to use TAI or an equivalent, but the mapping from TAI to UTC or
    /// local time depends on leap seconds, so you will not have exact agreement with wall-clock time.
    static boost::posix_time::ptime second_clock_time();

    bool is_special() const { return initTime_.is_special(); }

private:
    void assign(const Calendar& rhs);

    boost::posix_time::ptime initTime_;      // When calendar was started, suite time(could be in the past OR real time)
    boost::posix_time::ptime suiteTime_;     // The suite time for hybrid DATE does not change.
    boost::posix_time::ptime initLocalTime_; // Real Time: When calendar was started, used to work out duration_
    boost::posix_time::ptime lastTime_;      // Real Time: Used to calculate calendarIncrement
    boost::posix_time::time_duration
        duration_; // duration since last call to init/begin, used on Node for late and autocancel
    boost::posix_time::time_duration increment_{0, 1, 0, 0};

private:
    void update_cache() const;
    mutable int day_of_week_{-1};  // Cache
    mutable int day_of_year_{-1};  // Cache
    mutable int day_of_month_{-1}; // Cache
    mutable int month_{-1};        // Cache
    mutable int year_{-1};         // Cache

    Clock_t ctype_{Calendar::REAL}; // *NOT* persisted: can be derived from suite clock attribute
    bool dayChanged_{false};

private:
    // Note: The *only* reason to serialise the calendar is so that we can support
    // why() command on the client side. By default, calendar is initialised in the *server*
    // at begin time, from the clock attribute
    friend class cereal::access;
    template <class Archive>
    void serialize(Archive& ar, std::uint32_t const /*version*/);
};
} // namespace ecf

#endif /* ecflow_core_Calendar_HPP */
