// Copyright 2022 Memgraph Ltd.
//
// Use of this software is governed by the Business Source License
// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
// License, and you may not use this file except in compliance with the Business Source License.
//
// As of the Change Date specified in that file, in accordance with
// the Business Source License, use of this software will be governed
// by the Apache License, Version 2.0, included in the file
// licenses/APL.txt.

#pragma once

#include <chrono>
#include <cstdint>
#include <iostream>
#include <limits>

#include "fmt/format.h"
#include "utils/exceptions.hpp"
#include "utils/logging.hpp"

namespace memgraph::utils {

template <typename T>
concept Chrono = requires(T) {
  typename T::rep;
  typename T::period;
};

template <Chrono TFirst, Chrono TSecond>
constexpr auto GetAndSubtractDuration(TSecond &base_duration) {
  const auto duration = std::chrono::duration_cast<TFirst>(base_duration);
  base_duration -= duration;
  return duration.count();
}

template <typename TType>
bool Overflows(const TType &lhs, const TType &rhs) {
  if (lhs > 0 && rhs > 0 && lhs > (std::numeric_limits<TType>::max() - rhs)) [[unlikely]] {
    return true;
  }
  return false;
}

template <typename TType>
bool Underflows(const TType &lhs, const TType &rhs) {
  if (lhs < 0 && rhs < 0 && lhs < (std::numeric_limits<TType>::min() - rhs)) [[unlikely]] {
    return true;
  }
  return false;
}

namespace temporal {
struct InvalidArgumentException : public utils::BasicException {
  using utils::BasicException::BasicException;
};
}  // namespace temporal

struct DurationParameters {
  double day{0};
  double hour{0};
  double minute{0};
  double second{0};
  double millisecond{0};
  double microsecond{0};
};

DurationParameters ParseDurationParameters(std::string_view string);

struct Date;
struct LocalTime;
struct LocalDateTime;

struct Duration {
  explicit Duration(int64_t microseconds);
  explicit Duration(const DurationParameters &parameters);

  auto operator<=>(const Duration &) const = default;

  int64_t Days() const;
  int64_t SubDaysAsSeconds() const;
  int64_t SubDaysAsHours() const;
  int64_t SubDaysAsMinutes() const;
  int64_t SubDaysAsMilliseconds() const;
  int64_t SubDaysAsMicroseconds() const;
  int64_t SubDaysAsNanoseconds() const;
  int64_t SubSecondsAsNanoseconds() const;

  std::string ToString() const;

  friend std::ostream &operator<<(std::ostream &os, const Duration &dur) { return os << dur.ToString(); }

  Duration operator-() const;

  friend Duration operator+(const Duration &lhs, const Duration rhs) {
    if (Overflows(lhs.microseconds, rhs.microseconds)) {
      throw utils::BasicException("Duration arithmetic overflows");
    }

    if (Underflows(lhs.microseconds, rhs.microseconds)) {
      throw utils::BasicException("Duration arithmetic underflows");
    }

    return Duration(lhs.microseconds + rhs.microseconds);
  }

  friend Duration operator-(const Duration &lhs, const Duration rhs) { return lhs + (-rhs); }

  int64_t microseconds;
};

struct DurationHash {
  size_t operator()(const Duration &duration) const;
};

struct DateParameters {
  int64_t year{0};
  int64_t month{1};
  int64_t day{1};

  bool operator==(const DateParameters &) const = default;
};

// boolean indicates whether the parsed string was in extended format
std::pair<DateParameters, bool> ParseDateParameters(std::string_view date_string);

constexpr std::chrono::year_month_day ToChronoYMD(uint16_t year, uint8_t month, uint8_t day) {
  namespace chrono = std::chrono;
  return chrono::year_month_day(chrono::year(year), chrono::month(month), chrono::day(day));
}

constexpr std::chrono::sys_days ToChronoSysDaysYMD(uint16_t year, uint8_t month, uint8_t day) {
  return std::chrono::sys_days(ToChronoYMD(year, month, day));
}

constexpr std::chrono::days DaysSinceEpoch(uint16_t year, uint8_t month, uint8_t day) {
  return ToChronoSysDaysYMD(year, month, day).time_since_epoch();
}

struct Date {
  explicit Date() : Date{DateParameters{}} {}
  // we assume we accepted date in microseconds which was normalized using the epoch time point
  explicit Date(int64_t microseconds);
  explicit Date(const DateParameters &date_parameters);

  friend std::ostream &operator<<(std::ostream &os, const Date &date) { return os << date.ToString(); }

  int64_t MicrosecondsSinceEpoch() const;
  int64_t DaysSinceEpoch() const;
  std::string ToString() const;

  friend Date operator+(const Date &date, const Duration &dur) {
    namespace chrono = std::chrono;
    const auto date_as_duration = Duration(date.MicrosecondsSinceEpoch());
    const auto result = date_as_duration + dur;
    const auto ymd = chrono::year_month_day(chrono::sys_days(chrono::days(result.Days())));
    return Date({static_cast<int>(ymd.year()), static_cast<unsigned>(ymd.month()), static_cast<unsigned>(ymd.day())});
  }

  friend Date operator+(const Duration &dur, const Date &date) { return date + dur; }

  friend Date operator-(const Date &date, const Duration &dur) { return date + (-dur); }

  friend Duration operator-(const Date &lhs, const Date &rhs) {
    namespace chrono = std::chrono;
    const auto lhs_days = utils::DaysSinceEpoch(lhs.year, lhs.month, lhs.day);
    const auto rhs_days = utils::DaysSinceEpoch(rhs.year, rhs.month, rhs.day);
    const auto days_elapsed = lhs_days - rhs_days;
    return Duration(chrono::duration_cast<chrono::microseconds>(days_elapsed).count());
  }

  auto operator<=>(const Date &) const = default;

  uint16_t year;
  uint8_t month;
  uint8_t day;
};

struct DateHash {
  size_t operator()(const Date &date) const;
};

struct LocalTimeParameters {
  int64_t hour{0};
  int64_t minute{0};
  int64_t second{0};
  int64_t millisecond{0};
  int64_t microsecond{0};

  bool operator==(const LocalTimeParameters &) const = default;
};

// boolean indicates whether the parsed string was in extended format
std::pair<LocalTimeParameters, bool> ParseLocalTimeParameters(std::string_view string);

struct LocalTime {
  explicit LocalTime() : LocalTime{LocalTimeParameters{}} {}
  explicit LocalTime(int64_t microseconds);
  explicit LocalTime(const LocalTimeParameters &local_time_parameters);

  std::chrono::microseconds SumLocalTimeParts() const;

  // Epoch means the start of the day, i,e, midnight
  int64_t MicrosecondsSinceEpoch() const;
  int64_t NanosecondsSinceEpoch() const;
  std::string ToString() const;

  auto operator<=>(const LocalTime &) const = default;

  friend std::ostream &operator<<(std::ostream &os, const LocalTime &lt) { return os << lt.ToString(); }

  friend LocalTime operator+(const LocalTime &local_time, const Duration &dur) {
    namespace chrono = std::chrono;
    auto rhs = dur.SubDaysAsMicroseconds();
    auto abs = [](auto value) { return (value >= 0) ? value : -value; };
    const auto lhs = local_time.MicrosecondsSinceEpoch();
    if (rhs < 0 && lhs < abs(rhs)) {
      static constexpr int64_t one_day_in_microseconds =
          chrono::duration_cast<chrono::microseconds>(chrono::days(1)).count();
      rhs = one_day_in_microseconds + rhs;
    }
    auto result = chrono::microseconds(lhs + rhs);
    const auto h = GetAndSubtractDuration<chrono::hours>(result) % 24;
    const auto m = GetAndSubtractDuration<chrono::minutes>(result);
    const auto s = GetAndSubtractDuration<chrono::seconds>(result);
    const auto milli = GetAndSubtractDuration<chrono::milliseconds>(result);
    const auto micro = result.count();
    return LocalTime(LocalTimeParameters{h, m, s, milli, micro});
  }

  friend LocalTime operator+(const Duration &dur, const LocalTime &local_time) { return local_time + dur; }

  friend LocalTime operator-(const LocalTime &local_time, const Duration &duration) { return local_time + (-duration); }

  friend Duration operator-(const LocalTime &lhs, const LocalTime &rhs) {
    Duration lhs_dur(lhs.MicrosecondsSinceEpoch());
    Duration rhs_dur(rhs.MicrosecondsSinceEpoch());
    return lhs_dur - rhs_dur;
  }

  uint8_t hour;
  uint8_t minute;
  uint8_t second;
  uint16_t millisecond;
  uint16_t microsecond;
};

struct LocalTimeHash {
  size_t operator()(const LocalTime &local_time) const;
};

std::pair<DateParameters, LocalTimeParameters> ParseLocalDateTimeParameters(std::string_view string);

struct LocalDateTime {
  explicit LocalDateTime(int64_t microseconds);
  explicit LocalDateTime(const DateParameters &date_parameters, const LocalTimeParameters &local_time_parameters);
  explicit LocalDateTime(const Date &date, const LocalTime &local_time);

  int64_t MicrosecondsSinceEpoch() const;
  int64_t SecondsSinceEpoch() const;  // seconds since epoch
  int64_t SubSecondsAsNanoseconds() const;
  std::string ToString() const;

  auto operator<=>(const LocalDateTime &) const = default;

  friend std::ostream &operator<<(std::ostream &os, const LocalDateTime &ldt) { return os << ldt.ToString(); }

  friend LocalDateTime operator+(const LocalDateTime &dt, const Duration &dur) {
    const auto local_date_time_as_duration = Duration(dt.MicrosecondsSinceEpoch());
    const auto result = local_date_time_as_duration + dur;
    namespace chrono = std::chrono;
    const auto ymd = chrono::year_month_day(chrono::sys_days(chrono::days(result.Days())));
    const auto date_part =
        Date({static_cast<int>(ymd.year()), static_cast<unsigned>(ymd.month()), static_cast<unsigned>(ymd.day())});
    const auto local_time_part = LocalTime(result.SubDaysAsMicroseconds());
    return LocalDateTime(date_part, local_time_part);
  }

  friend LocalDateTime operator+(const Duration &dur, const LocalDateTime &dt) { return dt + dur; }

  friend LocalDateTime operator-(const LocalDateTime &dt, const Duration &dur) { return dt + (-dur); }

  friend Duration operator-(const LocalDateTime &lhs, const LocalDateTime &rhs) {
    return Duration(lhs.MicrosecondsSinceEpoch()) - Duration(rhs.MicrosecondsSinceEpoch());
  }

  Date date;
  LocalTime local_time;
};

struct LocalDateTimeHash {
  size_t operator()(const LocalDateTime &local_date_time) const;
};

Date CurrentDate();
LocalTime CurrentLocalTime();
LocalDateTime CurrentLocalDateTime();
}  // namespace memgraph::utils
