/********************************************************************\
 * gnc-datetime.cpp -- Date and Time classes for GnuCash            *
 *                                                                  *
 * Copyright 2015 John Ralls <jralls@ceridwen.us>                   *
 *                                                                  *
 * This program is free software; you can redistribute it and/or    *
 * modify it under the terms of the GNU General Public License as   *
 * published by the Free Software Foundation; either version 2 of   *
 * the License, or (at your option) any later version.              *
 *                                                                  *
 * This program is distributed in the hope that it will be useful,  *
 * but WITHOUT ANY WARRANTY; without even the implied warranty of   *
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the    *
 * GNU General Public License for more details.                     *
 *                                                                  *
 * You should have received a copy of the GNU General Public License*
 * along with this program; if not, contact:                        *
 *                                                                  *
 * Free Software Foundation           Voice:  +1-617-542-5942       *
 * 51 Franklin Street, Fifth Floor    Fax:    +1-617-542-2652       *
 * Boston, MA  02110-1301,  USA       gnu@gnu.org                   *
 *                                                                  *
\********************************************************************/

#ifndef __GNC_DATETIME_HPP__
#define  __GNC_DATETIME_HPP__

#include <cstdint>
#include <memory>
#include <string>
#include <vector>
#include <functional>
#include <optional>

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

typedef struct
{
    int year;  //1400-9999
    int month; //1-12
    int day; //1-31
} gnc_ymd;

enum class DayPart {
    start,    // 00:00 local
    neutral,  // 10:59 UTC
    end,      // 23:59 local
};

class GncDateTimeImpl;
class GncDateImpl;
class GncDate;
using time64 = int64_t;
constexpr const time64 MINTIME = -17987443200;
constexpr const time64 MAXTIME = 253402214400;

/** GnuCash DateTime class
 *
 * Represents local time in the current timezone.
 * As with GncDate, the represented time is limited to the period
 * between 1400 and 9999 CE.
 *
 * Be careful when using times: A particular time is represented
 * differently depending on the timezone, which can shift the displayed
 * date. Accounting is generally not sensitive to the time of day, but
 * is sensitive to the recorded day. Since GncDates are not timezone
 * dependent they should be preferred for accounting entries.
 */

class GncDateTime
{
public:
/** Construct a GncDateTime representing the current time in the
 * current timezone.
 */
    GncDateTime();
/** Construct a GncDateTime in the current timezone representing the
 * timestamp as seconds from the POSIX epoch (1970-01-01T00:00:00UTC).
 * @param time Seconds from the POSIX epoch.
 * @exception std::invalid_argument if the year is outside the constraints.
 */
    GncDateTime(const time64 time);
/** Construct a GncDateTime in the current timezone representing the
 * standard struct tm provided.
 * @param tm A C-standard struct tm representing the date and
 * time. Note that the timezone and offset are ignored on those
 * systems which include them in struct tm.
 * @exception std::invalid_argument if the year is outside the constraints.
 */
    GncDateTime(const struct tm tm);
/** Construct a GncDateTime from a GncDate. As a GncDate doesn't contain time
 * information, the time will be set depending on the second parameter
 * to start of day, neutral or end of day.
 * @param date A GncDate representing a date.
 * @param part An optinoal DayPart indicating which time to use in the conversion.
 * This can be "DayPart::start" for start of day (00:00 local time),
 *             "DayPart::neutral" for a neutral time (10:59 UTC, chosen to have the
 *              least chance of date changes when crossing timezone borders),
 *             "DayPart::end" for end of day (23:59 local time).
 * If omitted part defaults to DayPart::neutral.
 * Note the different timezone used for DayPart::neutral compared to the other
 * two options!
 * @exception std::invalid_argument if the year is outside the constraints.
 */
    GncDateTime(const GncDate& date, DayPart part = DayPart::neutral);
/** Construct a GncDateTime
 * @param str A string representing the date and time in some
 * recognizable format. Note that if a timezone is not specified the
 * default is UTC, not the local one.
 * @exception std::invalid_argument if the year is outside the constraints.
 */
    GncDateTime(const std::string& str);
    GncDateTime(const char* str);
    ~GncDateTime();
/** Set the GncDateTime to the date and time indicated in the computer's clock.
 */
    void now();
/** Cast the GncDateTime to a time64, seconds from the POSIX epoch. */
    explicit operator time64() const;
/** Cast the GncDateTime to a struct tm. Timezone field isn't filled.
 */
    explicit operator struct tm() const;
/** Obtain the UTC offset in seconds
 *  @return seconds difference between this local time and UTC. West
 *  is negative.
 */
    long offset()const;
/** Obtain a struct tm representing the time in UTC.
 * @return struct tm
 */
    struct tm utc_tm() const;
/** Obtain the date from the time, as a GncDate, in the current timezone.
 *  @return GncDate represented by the GncDateTime.
 */
    GncDate date() const;
/** Test if the GncDateTime has a member pointer. Testing only. */
    bool isnull (void) { return m_impl == nullptr; }
/** Format the GncDateTime into a std::string
 *  @param format A cstr describing the way the date and time are
 *  presented. Code letters preceded with % stand in for arguments;
 *  most are the same as described in strftime(3), but there are a few
 *  differences. Consult the boost::date_time documentation.
 *  @return a std::string containing a representation of the date and time in
 *  the locale's time zone according to the format.
 */
    std::string format(const char* format) const;
/** Format the GncDateTime into a std::string in GMT
 *  @param format A cstr describing the way the date and time are
 *  presented. Code letters preceded with % stand in for arguments;
 *  most are the same as described in strftime(3), but there are a few
 *  differences. Consult the boost::date_time documentation.
 *  @return a std::string containing a representation of the date and time in
 *  GMT (timezone Z) according to the format.
 */
    std::string format_zulu(const char* format) const;
/** Format the GncDateTime into a gnucash-style iso8601 string in UTC.
 *  @return a std::string in the format YYYY-MM-DD HH:MM:SS.
 */
    std::string format_iso8601() const;
/** Get an undelimited string representing the current date and time.
 *  @return a std::string in the format YYYYMMDDHHMMSS.
 */
    static std::string timestamp();
    
private:
    std::unique_ptr<GncDateTimeImpl> m_impl;
};

/** GnuCash DateFormat class
 *
 * A helper class to represent a date format understood
 * by the GncDate string/format constructor. Consumers
 * of this header file are not supposed to create
 * objects of this class themselves. Instead they
 * can get a list of the understood formats from the
 * GncDate::c_formats class variable and work with those.
 */

using StringToDate = std::function<boost::gregorian::date(const std::string&)>;

class GncDateFormat
{
public:
    /** Construct a GncDateFormat with a given format and corresponding
     * regular expression. This should only be used internally by the
     * GncDate implementation. Consumers should never construct a GncDateFormat
     * themselves!
     */
    GncDateFormat (const char* fmt, const char* re) :
    m_fmt(fmt), m_re(re) {}
    GncDateFormat (const char* fmt, StringToDate str_to_date, const char* re) :
        m_fmt(fmt), m_re(re), m_str_to_date(str_to_date) {}
    GncDateFormat (const char* fmt, StringToDate str_to_date) :
        m_fmt(fmt), m_str_to_date(str_to_date) {}
    /** A string representing the format. */
    const std::string m_fmt;
private:
    /** Regular expression associated with the format string. This is to and
     * only be used internally by the gnc-datetime code.
     */
    const std::string m_re;
    std::optional<StringToDate> m_str_to_date;

    friend class GncDateImpl;
};

/** GnuCash Date class
 *
 * The represented date is limited to the period
 * between 1400 and 9999 CE.
 */

class GncDate
{
    public:
        /** A vector with all the date formats supported by the string constructor.
         * The currently supported formats are:
         * "y-m-d" (including yyyymmdd)
         * "d-m-y" (including ddmmyyyy)
         * "m-d-y" (including mmddyyyy)
         * "d-m"   (including ddmm)
         * "m-d"   (including mmdd)
         *
         * Notes:
         * - while the format names are using a "-" as separator, the
         * regexes will accept any of "-/.' " and will also work for dates
         * without separators.
         * - the format strings are marked for translation so it is possible
         * to use a localized version of a format string using gettext. Example:
         * gettext(GncDate::c_formats[0])
         */
        static const std::vector<GncDateFormat> c_formats;
        /** Construct a GncDate representing the current day.
         */
        GncDate();
        /** Construct a GncDate representing the given year, month, and day in
         * the proleptic Gregorian calendar.
         *
         * Years are constrained to be from 1400 - 9999 CE inclusive. Dates
         * will be normalized if the day or month values are outside of the
         * normal ranges. e.g. 1994, -3, 47 will be normalized to 1993-10-17.
         *
         * @param year The year in the Common Era.
         * @param month The month, where 1 is January and 12 is December.
         * @param day The day of the month, beginning with 1.
         * @exception std::invalid_argument if the calculated year is outside
         * of the constrained range.
         */
        GncDate(int year, int month, int day);
        /** Construct a GncDate by parsing a string assumed to be in the format
         * passed in.
         *
         * The currently recognized formats are d-m-y, m-d-y, y-m-d, m-d, d-m.
         * Note while the format descriptions use "-" as separator any of
         * "-" (hyphen), "/" (slash), "'" (single quote), " " (space) or
         * "." will be accepted.
         *
         * @param str The string to be interpreted.
         * @param fmt The expected date format of the string passed in.
         * @exception std::invalid_argument if
         * - the string couldn't be parsed using the provided format
         * - any of the date components is outside of its limit
         *   (like month being 13, or day being 31 in February)
         * - fmt doesn't specify a year, yet a year was found in the string
         */
        GncDate(const std::string str, const std::string fmt);
        /** Construct a GncDate from a GncDateImpl.
         */
        GncDate(std::unique_ptr<GncDateImpl> impl);
        /** Copy constructor.
         */
        GncDate(const GncDate&);
        /** Move constructor.
         */
        GncDate(GncDate&&);
        /** Default destructor.
         */
        ~GncDate();
        /** Copy assignment operator.
         */
        GncDate& operator=(const GncDate&);
        /** Move assignment operator.
         */
        GncDate& operator=(GncDate&&);
        /** Set the date object to the computer clock's current day. */
        void today();
        /** Get the year, month, and day from the date as a gnc_ymd.
         *  @return gnc_ymd struct
         */
        gnc_ymd year_month_day() const;
        /** Format the GncDate into a std::string
         *  @param format A cstr describing the way the date and time are
         *  presented. Code letters preceded with % stand in for arguments;
         *  most are the same as described in strftime(3), but there are a few
         *  differences. Consult the boost::date_time documentation.
         *  @return a std::string containing a representation of the date
         *  according to the format.
         */
        std::string format(const char* format);
        /** Test that the Date has an implementation. */
        bool isnull (void) { return m_impl == nullptr; }

private:
    std::unique_ptr<GncDateImpl> m_impl;

    friend GncDateTime::GncDateTime(const GncDate&, DayPart);
    friend bool operator<(const GncDate&, const GncDate&);
    friend bool operator>(const GncDate&, const GncDate&);
    friend bool operator==(const GncDate&, const GncDate&);
    friend bool operator<=(const GncDate&, const GncDate&);
    friend bool operator>=(const GncDate&, const GncDate&);
    friend bool operator!=(const GncDate&, const GncDate&);
};

/**@{
 *  Standard comparison operators working on GncDate objects.
 */
bool operator<(const GncDate& a, const GncDate& b);
bool operator>(const GncDate& a, const GncDate& b);
bool operator==(const GncDate& a, const GncDate& b);
bool operator<=(const GncDate& a, const GncDate& b);
bool operator>=(const GncDate& a, const GncDate& b);
bool operator!=(const GncDate& a, const GncDate& b);
/**@}*/

#endif // __GNC_DATETIME_HPP__
