#pragma once
#include "analytics.h"
#include "timing/piecewise_linear.h"
#include <cassert>
#include <optional>
#include <type_traits>

template <typename F, typename DomainTime, time_transform::IsTimeTransformer T>
auto boundedSubdivide(F func, const T &timeMap, DomainTime start,
                      double sampleRate, size_t frameBound) {
  using DomainTimeDelta =
      time_units::TypedTimeDelta<typename DomainTime::unit_tag,
                                 typename DomainTime::value_type>;
  using CodomainTime = typename T::CodomainTime;
  static_assert(std::is_same_v<DomainTime, typename T::DomainTime>,
                "DomainTime must be the same as the time map's domain time");
  static_assert(std::is_same_v<CodomainTime, time_units::Seconds<double>>,
                "CodomainTime must be Seconds<double>");

  auto currentPosition = start;
  auto currentFrame = 0;
  auto it = timeMap.getSegmentIteratorAt(currentPosition);

  constexpr int maxSegmentsWithoutProgress = 100;

  while (it != timeMap.end() && currentFrame < frameBound) {
    auto segment = *it;

    if (currentPosition < segment.source_range.start ||
        currentPosition >= segment.source_range.end) {
      // Somehow we're out of bounds. Reset to the start of the segment.
      currentPosition = segment.source_range.start;
    }

    auto mapped = segment.map_point(currentPosition);
    assert(mapped.has_value());

    auto mappedValue = *mapped;
    auto remainingSegmentDuration = segment.target_range.end - mappedValue;

    auto segmentDurationFrames = frameBound - currentFrame;

    if (remainingSegmentDuration.is_finite()) {
      segmentDurationFrames =
          std::min(segmentDurationFrames,
                   std::max(1UL, static_cast<size_t>(
                                     std::round(remainingSegmentDuration.raw() *
                                                (double)sampleRate))));
    }

    const auto segmentDurationDomain = DomainTimeDelta(
        (double)segmentDurationFrames / (sampleRate * segment.slope));

    std::optional<time_units::Beats<double>> maybeSeekTo =
        func(segment, currentPosition, mappedValue, currentFrame,
             segmentDurationFrames, segmentDurationDomain);

    currentPosition += segmentDurationDomain;
    currentFrame += segmentDurationFrames;

    if (maybeSeekTo.has_value()) {
      currentPosition = *maybeSeekTo;
      it = timeMap.getSegmentIteratorAt(currentPosition);
      continue;
    }

    int segmentsWithoutProgress = 0;
    while (currentPosition >= segment.source_range.end &&
           segmentsWithoutProgress++ < maxSegmentsWithoutProgress) {
      ++it;
      if (it == timeMap.end()) {
        break;
      }
      segment = *it;
    }

    assert(segmentsWithoutProgress < maxSegmentsWithoutProgress);
  }

  return currentPosition;
}

struct RenderContext {
  RenderContext(time_units::Beats<double> position, double bps,
                bool isContinuous,
                const time_transform::TimelineTempoMap &globalBeatsToSeconds,
                AnalyticsObserver *analyticsObserver,
                time_units::BeatsDelta<double> duration)
      : position(position), bps(bps), isContinuous(isContinuous),
        globalBeatsToSeconds(globalBeatsToSeconds),
        analyticsObserver(analyticsObserver), duration(duration) {}
  time_units::Beats<double> position;
  double bps;
  bool isContinuous;
  const time_transform::TimelineTempoMap &globalBeatsToSeconds;
  AnalyticsObserver *analyticsObserver;
  time_units::BeatsDelta<double> duration;
};