#pragma once
#include "timing/time_units.h"
#include <cassert>
#include <optional>

namespace time_transform {

using namespace time_units;

namespace SegmentMarks {
using MaskType = uint32_t;
constexpr MaskType NONE = 0;
constexpr MaskType INVALID_ARRANGEMENT_TIME = 1 << 0;
constexpr MaskType SEEK_TO_LOOP_START = 1 << 1;
} // namespace SegmentMarks

// Represents a time range [start, end).
template <typename Unit, TimeValue T = double> struct TimeRange {
  using TimePoint = TypedTime<Unit, T>;
  using TimeDelta = TypedTimeDelta<Unit, T>;

  TimePoint start;
  TimePoint end;

  bool contains(const TimePoint &t, bool include_end = false) const {
    if (include_end) {
      return t >= start && t <= end;
    } else {
      return t >= start && t < end;
    }
  }

  bool is_finite() const { return start.is_finite() && end.is_finite(); }

  TimeDelta duration() const { return end - start; }

  [[nodiscard]]
  std::optional<TimeRange> intersect(const TimeRange &other) const {
    // Determine latest start time and earliest end time
    TimePoint latest_start = std::max(start, other.start);
    TimePoint earliest_end = std::min(end, other.end);

    // Check if intersection is valid (start < end)
    if (latest_start < earliest_end) {
      return TimeRange{latest_start, earliest_end};
    }

    return std::nullopt;
  }

  std::string toString() const {
    return "[" + start.toString() + ", " + end.toString() + ")";
  }
};

// Represents a single linear segment of a time transformation.
// Maps source_range to target_range.
template <typename DomainUnit, typename CodomainUnit, TimeValue T = double>
struct MappedSegment {
  TimeRange<DomainUnit, T> source_range;
  TimeRange<CodomainUnit, T> target_range;

  // Needed in case the ranges are not bounded on one side
  T slope;
  bool left_anchored;
  SegmentMarks::MaskType mask;

  MappedSegment(const TimeRange<DomainUnit, T> &src_r,
                const TimeRange<CodomainUnit, T> &tgt_r, T slope,
                SegmentMarks::MaskType msk = SegmentMarks::NONE)
      : source_range(src_r), target_range(tgt_r), slope(slope),
        left_anchored(source_range.start.is_finite()), mask(msk) {
    // Cannot have mismatched infinite ends
    assert(source_range.start.is_finite() == target_range.start.is_finite());
    assert(source_range.end.is_finite() == target_range.end.is_finite());
    // Cannot have both ends infinite
    assert(source_range.start.is_finite() || source_range.end.is_finite());
    assert(!std::isnan(slope));
  }

public:
  using DomainTime = time_units::TypedTime<DomainUnit, T>;
  using DomainDelta = time_units::TypedTimeDelta<DomainUnit, T>;
  using CodomainTime = time_units::TypedTime<CodomainUnit, T>;
  using CodomainDelta = time_units::TypedTimeDelta<CodomainUnit, T>;

  // Map point from domain to codomain within this segment
  [[nodiscard]]
  std::optional<CodomainTime> map_point(const DomainTime &p,
                                        bool include_end = false) const {
    if (!source_range.contains(p, include_end)) {
      return std::nullopt;
    }

    if (!std::isfinite(slope)) {
      return target_range.end;
    } else if (slope == 0.0) {
      return target_range.start;
    }

    if (left_anchored) {
      return CodomainDelta((slope * (p - source_range.start)).raw()) +
             target_range.start;
    } else {
      return CodomainDelta((slope * (p - source_range.end)).raw()) +
             target_range.end;
    }
  }

  // Map point from codomain back to domain within this segment
  [[nodiscard]]
  std::optional<DomainTime> inverse_map_point(const CodomainTime &p,
                                              bool include_end = false) const {
    if (!target_range.contains(p, include_end)) {
      return std::nullopt;
    }

    if (!std::isfinite(slope)) {
      return source_range.start;
    } else if (slope == 0.0) {
      return source_range.end;
    }

    if (left_anchored) {
      return DomainDelta((p - target_range.start).raw() / slope) +
             source_range.start;
    } else {
      return DomainDelta((p - target_range.end).raw() / slope) +
             source_range.end;
    }
  }

  std::string toString() const {
    return "MappedSegment(" + source_range.toString() + " -> " +
           target_range.toString() + " m=" + std::to_string(slope) +
           " anchor=" + (left_anchored ? "left" : "right") +
           " mask=" + std::to_string(mask) + ")";
  }

  bool has_mark(SegmentMarks::MaskType bit_to_check) const noexcept {
    return (mask & bit_to_check) != 0;
  }
  SegmentMarks::MaskType get_mask() const noexcept { return mask; }
};

// Concept to ensure a type behaves like a time transformer with an iterator
// interface
template <typename TT>
concept IsTimeTransformer = requires(
    const TT &t, TT &mutable_t, const typename TT::DomainTime &domain_time) {
  // Check for FromTime and ToTime type aliases that satisfy IsTypedTime
  typename TT::DomainTime;
  requires IsTypedTime<typename TT::DomainTime>;
  typename TT::CodomainTime;
  requires IsTypedTime<typename TT::CodomainTime>;

  // Check for consistent value_type (e.g., both double)
  requires std::is_same_v<typename TT::DomainTime::value_type,
                          typename TT::CodomainTime::value_type>;

  // Check for iterator type and begin/end methods
  typename TT::iterator;
  {
    mutable_t.begin()
  } -> std::same_as<typename TT::iterator>;             // Check non-const begin
  { t.begin() } -> std::same_as<typename TT::iterator>; // Check const begin
  {
    mutable_t.end()
  } -> std::same_as<typename TT::iterator>;           // Check non-const end
  { t.end() } -> std::same_as<typename TT::iterator>; // Check const end

  // Check for getSegmentIteratorAt (const version)
  {
    t.getSegmentIteratorAt(domain_time)
  } -> std::same_as<typename TT::iterator>;

  // Further checks on the iterator itself
  requires std::bidirectional_iterator<
      typename TT::iterator>; // Require bidirectional iterator
  requires std::same_as<
      typename std::iterator_traits<typename TT::iterator>::value_type,
      MappedSegment<typename TT::DomainTime::unit_tag,
                    typename TT::CodomainTime::unit_tag,
                    typename TT::DomainTime::value_type>>;
};

} // namespace time_transform