///|
/// One detector observation supplied to a tracker update.
pub struct Detection {
  bbox : BoundingBox
  score : Double
  class_id : Int
} derive(Debug, Eq)

///|
fn Detection::valid(self : Detection) -> Bool {
  self.bbox.valid() &&
  is_finite(self.score) &&
  self.score >= 0.0 &&
  self.score <= 1.0 &&
  self.class_id >= 0
}

///|
/// Creates a detection after validating its score and class identifier.
pub fn Detection::new(
  bbox : BoundingBox,
  score : Double,
  class_id : Int,
) -> Detection raise TrackerError {
  if !bbox.valid() {
    raise InvalidBoundingBox("detection contains an invalid box")
  }
  if !is_finite(score) || score < 0.0 || score > 1.0 {
    raise InvalidScore(score)
  }
  if class_id < 0 {
    raise InvalidClassId(class_id)
  }
  { bbox, score, class_id, }
}

///|
/// Returns the detection box.
pub fn Detection::bbox(self : Detection) -> BoundingBox {
  self.bbox
}

///|
/// Returns the detector confidence in the closed interval `[0, 1]`.
pub fn Detection::score(self : Detection) -> Double {
  self.score
}

///|
/// Returns the non-negative detector class identifier.
pub fn Detection::class_id(self : Detection) -> Int {
  self.class_id
}

///|
/// Settings that determine detection selection and identity association.
pub struct TrackerConfig {
  high_score_threshold : Double
  low_score_threshold : Double
  new_track_threshold : Double
  first_match_max_cost : Double
  second_match_max_cost : Double
  max_lost_frames : Int
  min_hits : Int
  fuse_score : Bool
} derive(Debug, Eq)

///|
fn valid_unit_interval(value : Double) -> Bool {
  is_finite(value) && value >= 0.0 && value <= 1.0
}

///|
/// Creates a complete tracker configuration.
pub fn TrackerConfig::new(
  high_score_threshold~ : Double,
  low_score_threshold~ : Double,
  new_track_threshold~ : Double,
  first_match_max_cost~ : Double,
  second_match_max_cost~ : Double,
  max_lost_frames~ : Int,
  min_hits~ : Int,
  fuse_score~ : Bool,
) -> TrackerConfig raise TrackerError {
  if !valid_unit_interval(high_score_threshold) ||
    !valid_unit_interval(low_score_threshold) ||
    !valid_unit_interval(new_track_threshold) {
    raise InvalidConfig("score thresholds must be finite values in [0, 1]")
  }
  if low_score_threshold > high_score_threshold {
    raise InvalidConfig(
      "low_score_threshold must not exceed high_score_threshold",
    )
  }
  if !valid_unit_interval(first_match_max_cost) ||
    !valid_unit_interval(second_match_max_cost) {
    raise InvalidConfig("matching costs must be finite values in [0, 1]")
  }
  if max_lost_frames < 0 {
    raise InvalidConfig("max_lost_frames must be non-negative")
  }
  if min_hits < 1 {
    raise InvalidConfig("min_hits must be at least 1")
  }
  {
    high_score_threshold,
    low_score_threshold,
    new_track_threshold,
    first_match_max_cost,
    second_match_max_cost,
    max_lost_frames,
    min_hits,
    fuse_score,
  }
}

///|
/// Returns MBMOT's default association settings.
pub fn TrackerConfig::default() -> TrackerConfig {
  {
    high_score_threshold: 0.25,
    low_score_threshold: 0.10,
    new_track_threshold: 0.25,
    first_match_max_cost: 0.80,
    second_match_max_cost: 0.50,
    max_lost_frames: 30,
    min_hits: 1,
    fuse_score: true,
  }
}

///|
/// Returns the high-confidence cutoff used by the first association stage.
pub fn TrackerConfig::high_score_threshold(self : TrackerConfig) -> Double {
  self.high_score_threshold
}

///|
/// Returns the low-confidence cutoff reserved for the second association stage.
pub fn TrackerConfig::low_score_threshold(self : TrackerConfig) -> Double {
  self.low_score_threshold
}

///|
/// Returns the minimum score allowed to create a new track.
pub fn TrackerConfig::new_track_threshold(self : TrackerConfig) -> Double {
  self.new_track_threshold
}

///|
/// Returns the maximum accepted first-stage assignment cost.
pub fn TrackerConfig::first_match_max_cost(self : TrackerConfig) -> Double {
  self.first_match_max_cost
}

///|
/// Returns the maximum second-stage cost reserved for low-score recovery.
pub fn TrackerConfig::second_match_max_cost(self : TrackerConfig) -> Double {
  self.second_match_max_cost
}

///|
/// Returns the configured number of frames retained after a track is lost.
pub fn TrackerConfig::max_lost_frames(self : TrackerConfig) -> Int {
  self.max_lost_frames
}

///|
/// Returns the number of hits required before a track becomes visible.
pub fn TrackerConfig::min_hits(self : TrackerConfig) -> Int {
  self.min_hits
}

///|
/// Reports whether detector scores are fused into first-stage costs.
pub fn TrackerConfig::fuse_score(self : TrackerConfig) -> Bool {
  self.fuse_score
}

///|
/// A visible tracked object for one processed frame.
pub struct Track {
  track_id : Int
  bbox : BoundingBox
  class_id : Int
  score : Double
  first_frame : Int
  last_frame : Int
  hits : Int
} derive(Debug, Eq)

///|
/// Returns the stable identity assigned by this tracker instance.
pub fn Track::track_id(self : Track) -> Int {
  self.track_id
}

///|
/// Returns the box observed in the current frame.
pub fn Track::bbox(self : Track) -> BoundingBox {
  self.bbox
}

///|
/// Returns the class identifier fixed when the track was created.
pub fn Track::class_id(self : Track) -> Int {
  self.class_id
}

///|
/// Returns the most recent detector confidence.
pub fn Track::score(self : Track) -> Double {
  self.score
}

///|
/// Returns the frame in which the identity was created.
pub fn Track::first_frame(self : Track) -> Int {
  self.first_frame
}

///|
/// Returns the most recent frame associated with the identity.
pub fn Track::last_frame(self : Track) -> Int {
  self.last_frame
}

///|
/// Returns the number of detector observations assigned to the identity.
pub fn Track::hits(self : Track) -> Int {
  self.hits
}

///|
/// The visible tracks and lifecycle events produced by one update.
struct FrameTracks {
  tracks : Array[Track]
  lost : Array[Int]
  removed : Array[Int]
} derive(Debug, Eq)

///|
/// Returns visible tracks sorted by `track_id`.
pub fn FrameTracks::tracks(self : FrameTracks) -> Array[Track] {
  self.tracks.copy()
}

///|
/// Returns identities that entered the recoverable lost state in this frame.
pub fn FrameTracks::lost(self : FrameTracks) -> Array[Int] {
  self.lost.copy()
}

///|
/// Returns identities permanently removed in this frame.
pub fn FrameTracks::removed(self : FrameTracks) -> Array[Int] {
  self.removed.copy()
}

///|
/// A read-only summary of identities retained by a tracker.
struct TrackerStatus {
  last_frame : Int?
  active_count : Int
  tentative_count : Int
  lost_count : Int
}

///|
/// Returns the most recently accepted frame, or `None` before the first update.
pub fn TrackerStatus::last_frame(self : TrackerStatus) -> Int? {
  self.last_frame
}

///|
/// Returns the number of confirmed identities visible in the latest frame.
pub fn TrackerStatus::active_count(self : TrackerStatus) -> Int {
  self.active_count
}

///|
/// Returns the number of active identities still below `min_hits`.
pub fn TrackerStatus::tentative_count(self : TrackerStatus) -> Int {
  self.tentative_count
}

///|
/// Returns the number of confirmed identities retained for recovery.
pub fn TrackerStatus::lost_count(self : TrackerStatus) -> Int {
  self.lost_count
}