#pragma once
#include <cmath>
#include <concepts>
#include <limits>
#include <stdexcept>
#include <string>
#include <type_traits>

namespace time_units {

constexpr double EPSILON = 1e-10;

// Base concept for all time values
template <typename T>
concept TimeValue = requires(T a, T b) {
  { a + b } -> std::convertible_to<T>;
  { a - b } -> std::convertible_to<T>;
  { a * double{} } -> std::convertible_to<T>;
  { a / double{} } -> std::convertible_to<T>;
  { a < b } -> std::convertible_to<bool>;
  { std::numeric_limits<T>::infinity() } -> std::convertible_to<T>;
  { -std::numeric_limits<T>::infinity() } -> std::convertible_to<T>;
};

// Time unit tags (what is being measured)
struct SecondsTag {};
struct BeatsTag {};

// Intervals represent durations without a reference point
template <typename UnitTag, TimeValue T = double> class TypedTimeDelta final {
private:
  T value_;

public:
  using unit_tag = UnitTag;
  using value_type = T;

  explicit TypedTimeDelta(T value) noexcept : value_(value) {}

  // Allow access to the raw value
  T raw() const noexcept { return value_; }

  bool is_finite() const noexcept { return std::isfinite(value_); }

  // Basic arithmetic operations
  TypedTimeDelta operator+(const TypedTimeDelta &other) const noexcept {
    return TypedTimeDelta(value_ + other.value_);
  }

  TypedTimeDelta &operator+=(const TypedTimeDelta &other) noexcept {
    value_ += other.value_;
    return *this;
  }

  TypedTimeDelta operator-(const TypedTimeDelta &other) const noexcept {
    return TypedTimeDelta(value_ - other.value_);
  }

  TypedTimeDelta &operator-=(const TypedTimeDelta &other) noexcept {
    value_ -= other.value_;
    return *this;
  }

  TypedTimeDelta operator*(double scalar) const noexcept {
    return TypedTimeDelta(value_ * scalar);
  }

  friend TypedTimeDelta operator*(double scalar,
                                  const TypedTimeDelta &delta) noexcept {
    return delta * scalar;
  }

  TypedTimeDelta operator-() const noexcept { return TypedTimeDelta(-value_); }

  TypedTimeDelta operator/(double scalar) const {
    if (std::abs(scalar) < EPSILON) {
      throw std::invalid_argument("Division by zero or very small value");
    }
    return TypedTimeDelta(value_ / scalar);
  }

  // Comparison operators
  bool operator<(const TypedTimeDelta &other) const noexcept {
    return value_ < other.value_;
  }
  bool operator>(const TypedTimeDelta &other) const noexcept {
    return value_ > other.value_;
  }
  bool operator<=(const TypedTimeDelta &other) const noexcept {
    return value_ <= other.value_;
  }
  bool operator>=(const TypedTimeDelta &other) const noexcept {
    return value_ >= other.value_;
  }
  bool operator==(const TypedTimeDelta &other) const noexcept {
    if (value_ == std::numeric_limits<T>::infinity() &&
        other.value_ == std::numeric_limits<T>::infinity()) {
      return true;
    }
    if (value_ == -std::numeric_limits<T>::infinity() &&
        other.value_ == -std::numeric_limits<T>::infinity()) {
      return true;
    }
    return std::abs(value_ - other.value_) < EPSILON;
  }

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

  std::string toString() const { return "d" + std::to_string(value_); }
};

// Strong typing for time points
template <typename UnitTag, TimeValue T = double> class TypedTime final {
private:
  T value_;

public:
  using unit_tag = UnitTag;
  using value_type = T;

  explicit TypedTime(T value) noexcept : value_(value) {}

  // Allow access to the raw value
  T raw() const noexcept { return value_; }

  bool is_finite() const noexcept { return std::isfinite(value_); }

  // Addition with interval of the same unit
  TypedTime operator+(const TypedTimeDelta<UnitTag, T> &delta) const noexcept {
    return TypedTime(value_ + delta.raw());
  }

  friend TypedTime operator+(const TypedTimeDelta<UnitTag, T> &delta,
                             const TypedTime &time) noexcept {
    return time + delta;
  }

  TypedTime &operator+=(const TypedTimeDelta<UnitTag, T> &delta) noexcept {
    value_ += delta.raw();
    return *this;
  }

  // Subtraction of two time points yields an interval
  TypedTimeDelta<UnitTag, T> operator-(const TypedTime &other) const noexcept {
    return TypedTimeDelta<UnitTag, T>(value_ - other.value_);
  }

  // Subtraction of an interval yields a time point
  TypedTime operator-(const TypedTimeDelta<UnitTag, T> &delta) const noexcept {
    return TypedTime(value_ - delta.raw());
  }

  TypedTime &operator-=(const TypedTimeDelta<UnitTag, T> &delta) noexcept {
    value_ -= delta.raw();
    return *this;
  }

  // Comparison operators
  bool operator<(const TypedTime &other) const noexcept {
    return value_ < other.value_;
  }

  bool operator>(const TypedTime &other) const noexcept {
    return value_ > other.value_;
  }

  bool operator<=(const TypedTime &other) const noexcept {
    return value_ <= other.value_;
  }

  bool operator>=(const TypedTime &other) const noexcept {
    return value_ >= other.value_;
  }

  bool operator==(const TypedTime &other) const noexcept {
    if (value_ == std::numeric_limits<T>::infinity() &&
        other.value_ == std::numeric_limits<T>::infinity()) {
      return true;
    }
    if (value_ == -std::numeric_limits<T>::infinity() &&
        other.value_ == -std::numeric_limits<T>::infinity()) {
      return true;
    }
    return std::abs(value_ - other.value_) < EPSILON;
  }

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

  std::string toString() const { return std::to_string(value_); }

  static TypedTime inf() noexcept {
    return TypedTime(std::numeric_limits<T>::infinity());
  }

  static TypedTime neg_inf() noexcept {
    return TypedTime(-std::numeric_limits<T>::infinity());
  }
};

// Concept to check if a type is a valid TypedTime specialization
template <typename TTT>
concept IsTypedTime = requires {
  typename TTT::unit_tag;
  typename TTT::value_type;
  requires std::is_same_v<
      TTT, TypedTime<typename TTT::unit_tag, typename TTT::value_type>>;
  requires TimeValue<typename TTT::value_type>;
};

// Type aliases for common time domains
template <TimeValue T = double>
using SecondsDelta = TypedTimeDelta<SecondsTag, T>;

template <TimeValue T = double> using BeatsDelta = TypedTimeDelta<BeatsTag, T>;

template <TimeValue T = double> using Seconds = TypedTime<SecondsTag, T>;

template <TimeValue T = double> using Beats = TypedTime<BeatsTag, T>;

} // namespace time_units
