#!/usr/bin/env python3
from __future__ import annotations

import math


def source_start_bounds(
    usable_start_seconds: float,
    usable_end_seconds: float,
    shot_duration_seconds: float,
    epsilon: float = 1e-9,
) -> tuple[float, float] | None:
    values = (usable_start_seconds, usable_end_seconds, shot_duration_seconds)
    if not all(math.isfinite(value) for value in values):
        raise ValueError("Source bounds and shot duration must be finite")
    if usable_end_seconds <= usable_start_seconds or shot_duration_seconds <= 0:
        raise ValueError("Source range and shot duration must be positive")
    latest_start = usable_end_seconds - shot_duration_seconds
    if latest_start < usable_start_seconds - epsilon:
        return None
    return usable_start_seconds, max(usable_start_seconds, latest_start)


def clamp_candidate_start(value: float, bounds: tuple[float, float]) -> float:
    if not math.isfinite(value):
        raise ValueError("Candidate start must be finite")
    return min(max(value, bounds[0]), bounds[1])


def frame_aligned_candidate_starts(
    value: float,
    bounds: tuple[float, float],
    frame_rate: float,
    radius_frames: int = 1,
) -> list[float]:
    """Return nearby starts on the source media's frame grid."""
    if not math.isfinite(frame_rate) or frame_rate <= 0:
        raise ValueError("Frame rate must be positive and finite")
    if radius_frames < 0:
        raise ValueError("Frame radius must be non-negative")

    center_frame = round(float(value) * frame_rate)
    starts: list[float] = []
    for frame_index in range(center_frame - radius_frames, center_frame + radius_frames + 1):
        aligned = clamp_candidate_start(frame_index / frame_rate, bounds)
        if not any(abs(existing - aligned) < 1e-9 for existing in starts):
            starts.append(aligned)
    return starts


def placement_safe_seconds(
    value: float,
    bounds: tuple[float, float],
    epsilon_seconds: float = 1e-6,
) -> float:
    """Nudge an internal frame boundary past floating-point floor ambiguity."""
    if epsilon_seconds < 0 or not math.isfinite(epsilon_seconds):
        raise ValueError("Placement epsilon must be non-negative and finite")
    clamped = clamp_candidate_start(value, bounds)
    if clamped <= bounds[0] + epsilon_seconds:
        return bounds[0]
    if clamped >= bounds[1] - epsilon_seconds:
        return bounds[1]
    return min(clamped + epsilon_seconds, bounds[1])


def placement_safe_range_seconds(
    start_seconds: float,
    end_seconds: float,
    bounds: tuple[float, float],
    epsilon_seconds: float = 1e-6,
) -> tuple[float, float]:
    """Nudge a complete source range without changing its duration.

    Adjusting in/out independently can lengthen or shorten a placement when
    only one boundary is at a media edge. Clamp the start to the interval in
    which the complete range fits, then apply the placement nudge to both
    boundaries together whenever there is room.
    """
    values = (start_seconds, end_seconds, bounds[0], bounds[1])
    if not all(math.isfinite(value) for value in values):
        raise ValueError("Placement range and bounds must be finite")
    if epsilon_seconds < 0 or not math.isfinite(epsilon_seconds):
        raise ValueError("Placement epsilon must be non-negative and finite")
    duration = end_seconds - start_seconds
    if duration <= 0:
        raise ValueError("Placement range must be positive")
    available_duration = bounds[1] - bounds[0]
    if duration > available_duration + 1e-9:
        raise ValueError("Placement range does not fit inside the source bounds")

    latest_start = max(bounds[0], bounds[1] - duration)
    clamped_start = min(max(start_seconds, bounds[0]), latest_start)
    safe_start = (
        min(clamped_start + epsilon_seconds, latest_start)
        if clamped_start < latest_start
        else clamped_start
    )
    return safe_start, safe_start + duration
