///|
priv struct TimedRecord {
  record : LapRecord
  cumulative_time_ms : Int
}

///|
fn cumulative_for(totals : Array[(String, Int)], driver : String) -> Int {
  for (known_driver, total) in totals {
    if known_driver == driver {
      return total
    }
  }
  0
}

///|
fn update_cumulative(
  totals : Array[(String, Int)],
  driver : String,
  cumulative_time_ms : Int,
) -> Unit {
  for index = 0; index < totals.length(); index = index + 1 {
    if totals[index].0 == driver {
      totals[index] = (driver, cumulative_time_ms)
      return
    }
  }
  totals.push((driver, cumulative_time_ms))
}

///|
fn time_records(data : RaceData) -> Array[TimedRecord] {
  let totals : Array[(String, Int)] = []
  let timed : Array[TimedRecord] = []
  for record in data.records {
    let cumulative_time_ms = cumulative_for(totals, record.driver) +
      record.lap_time_ms
    update_cumulative(totals, record.driver, cumulative_time_ms)
    timed.push({ record, cumulative_time_ms, })
  }
  timed
}

///|
fn rank_of(ranking : Array[TimedRecord], driver : String) -> Int {
  for index = 0; index < ranking.length(); index = index + 1 {
    if ranking[index].record.driver == driver {
      return index + 1
    }
  }
  0
}

///|
fn build_states(
  data : RaceData,
  drivers : Array[String],
) -> Array[DriverLapState] {
  let timed = time_records(data)
  let states : Array[DriverLapState] = []
  let driver_count = drivers.length()
  let lap_count = data.records.length() / driver_count
  for lap = 1; lap <= lap_count; lap = lap + 1 {
    let start = (lap - 1) * driver_count
    let ranking : Array[TimedRecord] = []
    for offset = 0; offset < driver_count; offset = offset + 1 {
      ranking.push(timed[start + offset])
    }
    ranking.sort_by((left, right) => {
      if left.cumulative_time_ms != right.cumulative_time_ms {
        left.cumulative_time_ms - right.cumulative_time_ms
      } else {
        left.record.driver.compare(right.record.driver)
      }
    })
    let leader_time_ms = ranking[0].cumulative_time_ms
    for offset = 0; offset < driver_count; offset = offset + 1 {
      let timed_record = timed[start + offset]
      let position = rank_of(ranking, timed_record.record.driver)
      let interval_to_ahead_ms = if position == 1 {
        None
      } else {
        Some(
          timed_record.cumulative_time_ms -
          ranking[position - 2].cumulative_time_ms,
        )
      }
      let record = timed_record.record
      states.push({
        lap: record.lap,
        driver: record.driver,
        lap_time_ms: record.lap_time_ms,
        cumulative_time_ms: timed_record.cumulative_time_ms,
        position,
        gap_to_leader_ms: timed_record.cumulative_time_ms - leader_time_ms,
        interval_to_ahead_ms,
        compound: record.compound,
        tyre_age_laps: record.tyre_age_laps,
        pit: record.pit,
        track_status: record.track_status,
        weather: record.weather,
      })
    }
  }
  states
}

///|
/// Reconstruct a deterministic end-of-lap timeline from validated CSV v1 data.
///
/// `data` must come from `parse_race_csv`; the result contains cumulative
/// milliseconds, positions, leader gaps, intervals, stints, and stable events.
/// A positive signed driver-to-driver gap means the named driver trails.
pub fn analyze_race(data : RaceData) -> RaceAnalysis {
  let drivers : Array[String] = []
  for record in data.records {
    if record.lap == 1 {
      drivers.push(record.driver)
    }
  }
  drivers.sort_by((left, right) => left.compare(right))
  let lap_count = data.records.length() / drivers.length()
  let states = build_states(data, drivers)
  let stints = build_stints(drivers, states)
  let events = build_events(drivers, states, lap_count)
  { drivers, lap_count, states, stints, events, }
}

///|
/// Find one driver's end-of-lap state, or `None` when the driver or lap is absent.
///
/// `analysis` is not mutated. The returned state is the end-of-lap state for
/// the exact `driver` and one-based `lap` query.
pub fn find_lap_state(
  analysis : RaceAnalysis,
  driver : String,
  lap : Int,
) -> DriverLapState? {
  for state in analysis.states {
    if state.driver == driver && state.lap == lap {
      return Some(state)
    }
  }
  None
}

///|
/// Return driver time minus opponent time at a lap, or `None` for an unknown query.
///
/// The result is in milliseconds: positive means `driver` trails `opponent`,
/// negative means `driver` leads, and zero means equal cumulative time.
pub fn signed_gap_ms(
  analysis : RaceAnalysis,
  driver : String,
  opponent : String,
  lap : Int,
) -> Int? {
  match
    (
      find_lap_state(analysis, driver, lap),
      find_lap_state(analysis, opponent, lap),
    ) {
    (Some(driver_state), Some(opponent_state)) =>
      Some(driver_state.cumulative_time_ms - opponent_state.cumulative_time_ms)
    _ => None
  }
}

///|
/// Return stints in number order for one driver; an unknown driver returns an empty array.
///
/// Each stint is reconstructed from validated end-of-lap states and is returned
/// in chronological stint-number order without mutating `analysis`.
pub fn stints_for_driver(
  analysis : RaceAnalysis,
  driver : String,
) -> Array[Stint] {
  let matching : Array[Stint] = []
  for stint in analysis.stints {
    if stint.driver == driver {
      matching.push(stint)
    }
  }
  matching
}