// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements.  See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership.  The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License.  You may obtain a copy of the License at
//
//   http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied.  See the License for the
// specific language governing permissions and limitations
// under the License.

#ifndef DORIS_BE_RUNTIME_DATETIME_VALUE_H
#define DORIS_BE_RUNTIME_DATETIME_VALUE_H

#include <stdint.h>

#include <iostream>
#include <cstddef>

#include "udf/udf.h"
#include "util/hash_util.hpp"

namespace doris {

enum TimeUnit {
    MICROSECOND,
    SECOND,
    MINUTE,
    HOUR,
    DAY,
    WEEK,
    MONTH,
    QUARTER,
    YEAR,
    SECOND_MICROSECOND,
    MINUTE_MICROSECOND,
    MINUTE_SECOND,
    HOUR_MICROSECOND,
    HOUR_SECOND,
    HOUR_MINUTE,
    DAY_MICROSECOND,
    DAY_SECOND,
    DAY_MINUTE,
    DAY_HOUR,
    YEAR_MONTH
};

struct TimeInterval {
    int32_t year;
    int32_t month;
    int32_t day;
    int32_t hour;
    int32_t minute;
    int32_t second;
    int32_t microsecond;
    bool is_neg;

    TimeInterval() :
            year(0), month(0), day(0), 
            hour(0), minute(0), second(0), microsecond(0), is_neg(false) {
    }

    TimeInterval(TimeUnit unit, int count, bool is_neg_param) :
            year(0), month(0), day(0), 
            hour(0), minute(0), second(0), microsecond(0), is_neg(is_neg_param) {
        switch (unit) {
        case YEAR:
            year = count;
            break;
        case MONTH:
            month = count;
            break;
        case WEEK:
            day = 7 * count;
            break;
        case DAY:
            day = count;
            break;
        case HOUR:
            hour = count;
            break;
        case MINUTE:
            minute = count;
            break;
        case SECOND:
            second = count;
            break;
        case MICROSECOND:
            microsecond = count;
            break;
        default:
            break;
        }
    }
};

enum TimeType {
    TIME_TIME = 1,
    TIME_DATE = 2,
    TIME_DATETIME = 3
};

// Used to compute week
const int WEEK_MONDAY_FIRST = 1;
const int WEEK_YEAR = 2;
const int WEEK_FIRST_WEEKDAY = 4;

// 9999-99-99 99:99:99.999999; 26 + 1('\0')
const int MAX_DTVALUE_STR_LEN = 27;

const int DATE_MAX_DAYNR = 3652424;
// two-digit years < this are 20..; >= this are 19..
const int YY_PART_YEAR = 70;

// Limits of time value
const int TIME_MAX_HOUR = 838;
const int TIME_MAX_MINUTE = 59;
const int TIME_MAX_SECOND = 59;
const int TIME_MAX_VALUE = 10000 * TIME_MAX_HOUR + 100 * TIME_MAX_MINUTE + TIME_MAX_SECOND;
const int TIME_MAX_VALUE_SECONDS = 3600 * TIME_MAX_HOUR + 60 * TIME_MAX_MINUTE + TIME_MAX_SECOND;

uint8_t mysql_week_mode(uint32_t mode);

class DateTimeValue {
public:
    // Constructor
    DateTimeValue() :
            _neg(0), _type(TIME_DATETIME), _hour(0), _minute(0), _second(0), 
            _year(0), _month(0), _day(0), _microsecond(0) {
    }

    DateTimeValue(int64_t t) {
        from_date_int64(t);
    }

    // Converted from Olap Date or Datetime
    bool from_olap_datetime(uint64_t datetime) {
        _neg = 0;
        _type = TIME_DATETIME;
        uint64_t date = datetime / 1000000;
        uint64_t time = datetime % 1000000;

        _year = date / 10000;
        date %= 10000;
        _month = date / 100;
        _day = date % 100;
        _hour = time / 10000;
        time %= 10000;
        _minute = time / 100;
        _second = time % 100;
        _microsecond = 0;

        if (check_range() || check_date()) {
            return false;
        }
        return true;
    }

    uint64_t to_olap_datetime() const {
        uint64_t date_val = _year * 10000 + _month * 100 + _day;
        uint64_t time_val = _hour * 10000 + _minute * 100 + _second;
        return date_val * 1000000 + time_val;
    }

    bool from_olap_date(uint64_t date) { 
        _neg = 0;
        _type = TIME_DATE;
        _hour = 0;
        _minute = 0;
        _second = 0;
        _microsecond = 0;

        _day = date & 0x1f;
        date >>= 5;

        _month = date & 0x0f;
        date >>= 4;

        _year = date;
        if (check_range() || check_date()) {
            return false;
        }
        return true;
    }

    uint64_t to_olap_date() const {
        uint64_t val;
        val = _year;
        val <<= 4;
        val |= _month;
        val <<= 5;
        val |= _day;
        return val;
    }

    bool from_date_format_str(const char* format, int format_len,
                              const char* value, int value_len) {
        memset(this, 0, sizeof(*this));
        return from_date_format_str(format, format_len, value, value_len, nullptr);
    }

    operator int64_t() const {
        return to_int64();
    }

    // Given days since 0000-01-01, construct the datetime value.
    bool from_date_daynr(uint64_t);

    // Construct Date/Datetime type value from string.
    // At least the following formats are recogniced (based on number of digits)
    // 'YYMMDD', 'YYYYMMDD', 'YYMMDDHHMMSS', 'YYYYMMDDHHMMSS'
    // 'YY-MM-DD', 'YYYY-MM-DD', 'YY-MM-DD HH.MM.SS'
    // 'YYYYMMDDTHHMMSS'
    bool from_date_str(const char* str, int len);

    // Construct Date/Datetime type value from int64_t value.
    // Return true if convert success. Otherwise return false.
    bool from_date_int64(int64_t value);

    // Construct time type value from int64_t value.
    // Return true if convert success. Otherwise return false.
    bool from_time_int64(int64_t value);

    // Convert this value to string
    // this will check type to decide which format to convert
    // TIME:  format 'hh:mm:ss.xxxxxx'
    // DATE:  format 'YYYY-MM-DD'
    // DATETIME:  format 'YYYY-MM-DD hh:mm:ss.xxxxxx'
    char* to_string(char* to) const;

    // Convert this datetime value to string by the format string
    bool to_format_string(const char* format, int len, char* to) const;
    int compute_format_len(const char* format, int len) const;

    // Convert this value to uint64_t
    // Will check its type
    int64_t to_int64() const;

    inline uint64_t daynr() const {
        return calc_daynr(_year, _month, _day);
    }

    // Calculate how many days since 0000-01-01
    // 0000-01-01 is 1st B.C.
    static uint64_t calc_daynr(uint32_t year, uint32_t month, uint32_t day);

    static uint8_t calc_weekday(uint64_t daynr, bool);

    int year() const {
        return _year;
    }
    int month() const {
        return _month;
    }
    int day() const {
        return _day;
    }
    int hour() const {
        return _hour;
    }
    int minute() const {
        return _minute;
    }
    int second() const {
        return _second;
    }

    void cast_to_date() {
        _hour = 0;
        _minute = 0;
        _second = 0;
        _microsecond = 0;
        _type = TIME_DATE;
    }

    void cast_to_time() {
        _year = 0;
        _month = 0;
        _day = 0;
        _type = TIME_TIME;
    }

    void to_datetime() {
        _type = TIME_DATETIME;
    }

    // Weekday, from 0(Mon) to 6(Sun)
    inline uint8_t weekday() const {
        return calc_weekday(daynr(), false);
    }

    // The bits in week_format has the following meaning:
    // WEEK_MONDAY_FIRST (0)  
    //  If not set: 
    //      Sunday is first day of week
    //  If set:     
    //      Monday is first day of week
    //
    // WEEK_YEAR (1)
    //  If not set: 
    //      Week is in range 0-53
    //      Week 0 is returned for the the last week of the previous year (for
    //      a date at start of january) In this case one can get 53 for the
    //      first week of next year.  This flag ensures that the week is
    //      relevant for the given year. Note that this flag is only
    //      relevant if WEEK_JANUARY is not set.
    //  If set: 
    //      Week is in range 1-53.
    //      In this case one may get week 53 for a date in January (when
    //      the week is that last week of previous year) and week 1 for a
    //      date in December.
    //      
    // WEEK_FIRST_WEEKDAY (2)  
    //  If not set
    //      Weeks are numbered according to ISO 8601:1988
    //  If set
    //      The week that contains the first 'first-day-of-week' is week 1.
    //
    // ISO 8601:1988 means that 
    //      if the week containing January 1 has
    //      four or more days in the new year, then it is week 1;
    //      Otherwise it is the last week of the previous year, and the
    //      next week is week 1.
    uint8_t week(uint8_t) const;

    // Add interval 
    bool date_add_interval(const TimeInterval& interval, TimeUnit unit);

    int unix_timestamp() const;

    bool from_unixtime(int);

    bool operator==(const DateTimeValue& other) const {
        // NOTE: This is not same with MySQL.
        // MySQL convert both to int with left value type and then compare
        // We think all fields equals.
        int64_t v1 = to_int64_datetime_packed();
        int64_t v2 = other.to_int64_datetime_packed();
        return v1 == v2;
    }

    bool operator!=(const DateTimeValue& other) const {
        return !(*this == other);
    }

    // Now, we don't support TIME_TIME type, 
    bool operator<=(const DateTimeValue& other) const {
        return !(*this > other);
    }

    bool operator>=(const DateTimeValue& other) const {
        return !(*this < other);
    }

    bool operator<(const DateTimeValue& other) const {
        int64_t v1 = to_int64_datetime_packed();
        int64_t v2 = other.to_int64_datetime_packed();
        return v1 < v2;
    }

    bool operator>(const DateTimeValue& other) const {
        int64_t v1 = to_int64_datetime_packed();
        int64_t v2 = other.to_int64_datetime_packed();
        return v1 > v2;
    }

    const char* month_name() const;

    const char* day_name() const;

    DateTimeValue& operator++() {
        switch (_type) {
        case TIME_DATE: {
            TimeInterval interval(DAY, 1, false);
            date_add_interval(interval, DAY);
            break;
        }
        case TIME_DATETIME: {
            TimeInterval interval(SECOND, 1, false);
            date_add_interval(interval, SECOND);
            break;
        }
        case TIME_TIME: {
            TimeInterval interval(SECOND, 1, false);
            date_add_interval(interval, SECOND);
            break;
        }
        }
        return *this;
    }

    void to_datetime_val(doris_udf::DateTimeVal* tv) const {
        tv->packed_time = to_int64_datetime_packed();
        tv->type = _type;
    }

    static DateTimeValue from_datetime_val(const doris_udf::DateTimeVal& tv) {
        DateTimeValue value;
        value.from_packed_time(tv.packed_time);
        if (tv.type == TIME_DATE) {
            value.cast_to_date();
        }
        return value;
    }

    inline uint32_t hash(int seed) const {
        return HashUtil::hash(this, sizeof(*this), seed);
    }

    int day_of_year() const {
        return daynr() - calc_daynr(_year, 1, 1) + 1;
    }

    // TODO(zhaochun): local time ???
    static DateTimeValue local_time();

    std::string debug_string() const {
        char buf[64];
        char* end = to_string(buf);
        return std::string(buf, end - buf);
    }

    static DateTimeValue datetime_min_value() {
        return _s_min_datetime_value;
    }

    static DateTimeValue datetime_max_value() {
        return _s_max_datetime_value;
    }

    int64_t second_diff(const DateTimeValue& rhs) const {
        return unix_timestamp() - rhs.unix_timestamp();
    }

    void set_type(int type);

    static const char* _s_llvm_class_name;

private:
    // Used to make sure sizeof DateTimeValue
    friend class UnusedClass;

    void from_packed_time(int64_t packed_time) {
        _microsecond = packed_time % (1LL << 24);
        int64_t ymdhms = packed_time >> 24;
        int64_t ymd = ymdhms >> 17;
        int64_t hms = ymdhms % (1 << 17);

        _day = ymd % (1 << 5);
        int64_t ym = ymd >> 5;
        _month = ym % 13;
        _year = ym / 13;
        _year %= 10000;
        _second = hms % (1 << 6);
        _minute = (hms >> 6) % (1 << 6);
        _hour = (hms >> 12);
        _neg = 0;
        _type = TIME_DATETIME;
    }

    int64_t make_packed_time(int64_t time, int64_t second_part) const {
        return (time << 24) + second_part;
    }

    // To compatitable with MySQL
    int64_t to_int64_datetime_packed() const {
        int64_t ymd = ((_year * 13 + _month) << 5) | _day;
        int64_t hms = (_hour << 12) | (_minute << 6) | _second;
        int64_t tmp = make_packed_time(((ymd << 17) | hms), _microsecond);
        return _neg ? -tmp : tmp;
    }

    int64_t to_int64_date_packed() const {
        int64_t ymd = ((_year * 13 + _month) << 5) | _day;
        int64_t tmp = make_packed_time(ymd << 17, 0);
        return _neg ? -tmp : tmp;
    }

    // Check wether value of field is valid.
    bool check_range() const;
    bool check_date() const;

    // Used to construct from int value
    int64_t standardlize_timevalue(int64_t value);

    // Used to convert to a string.
    char* append_date_string(char *to) const;
    char* append_time_string(char *to) const;
    char* to_datetime_string(char *to) const;
    char* to_date_string(char* to) const;
    char* to_time_string(char* to) const;

    // Used to convert to uint64_t
    int64_t to_datetime_int64() const;
    int64_t to_date_int64() const;
    int64_t to_time_int64() const;

    static uint8_t calc_week(const DateTimeValue& value, uint8_t mode, uint32_t *year);

    // This is private function which modify date but modify `_type`
    bool get_date_from_daynr(uint64_t);


    // Helper to set max, min, zero
    void set_zero(int type);
    void set_max_time(bool neg);

    bool from_date_format_str(const char* format, int format_len,
                              const char* value, int value_len, 
                              const char** sub_val_end);


    // 1 bits for neg. 3 bits for type. 12bit for hour
    uint16_t _neg:1;        // Used for time value.
    uint16_t _type:3;       // Which type of this value.
    uint16_t _hour:12;
    uint8_t _minute;
    uint8_t _second;
    uint16_t _year;
    uint8_t _month;
    uint8_t _day;
    // TODO(zc): used for nothing
    uint64_t _microsecond;

    DateTimeValue(uint8_t neg, uint8_t type, uint8_t hour, 
                  uint8_t minute, uint8_t second, uint32_t microsecond, 
                  uint16_t year, uint8_t month, uint8_t day) : 
            _neg(neg), _type(type), _hour(hour), _minute(minute), _second(second),
            _year(year), _month(month), _day(day), _microsecond(microsecond) { }

    static DateTimeValue _s_min_datetime_value;
    static DateTimeValue _s_max_datetime_value;
};

// only support DATE - DATE (no support DATETIME - DATETIME)
std::size_t operator-(const DateTimeValue& v1, const DateTimeValue& v2);

std::ostream& operator<<(std::ostream& os, const DateTimeValue& value);

std::size_t hash_value(DateTimeValue const& value);

}

namespace std {
    template<>
    struct hash<doris::DateTimeValue> {
        size_t operator()(const doris::DateTimeValue& v) const {
            return doris::hash_value(v);
        }
    };
}

#endif
