pub(super) const ANCHOR_ROW: f64 = 10.0;
pub(super) const ANCHOR_COL: f64 = 19.0;
pub(super) const CORE_ROW_OFFSET: f64 = 7.0;
pub(super) const CHAR_ASPECT_RATIO: f64 = 0.5;

pub(super) const CORE_GROW_SPEED: f64 = 0.2;
pub(super) const CORE_MAX_RADIUS: f64 = 2.0;
pub(super) const CORE_HOLD_TICKS: usize = 70;
pub(super) const CROSSFADE_TICKS: usize = 20;

#[derive(Clone, Debug)]
pub struct DistanceMap {
  pub(super) distances: Vec<Vec<f64>>,
  pub max_distance: f64,
}

pub fn build_distance_map(lines: &[&str]) -> DistanceMap {
  let mut max_distance: f64 = 0.0;
  let distances: Vec<Vec<f64>> = lines
    .iter()
    .enumerate()
    .map(|(row, line)| {
      line
        .chars()
        .enumerate()
        .map(|(col, ch)| char_distance(row, col, ch, &mut max_distance))
        .collect()
    })
    .collect();

  DistanceMap {
    distances,
    max_distance,
  }
}

fn char_distance(row: usize, col: usize, ch: char, max_distance: &mut f64) -> f64 {
  if ch == ' ' {
    return f64::INFINITY;
  }
  let d = euclidean_distance(row as f64, col as f64);
  if d > *max_distance {
    *max_distance = d;
  }
  d
}

pub fn distance_at(map: &DistanceMap, row: usize, col: usize) -> f64 {
  map.distances[row][col]
}

/// Weighted Euclidean distance from an arbitrary anchor point.
///
/// `deltas` is a slice of `(component_difference, weight)` pairs.
/// Generalises to any number of dimensions — 2-D terminal grids today,
/// 3-D projections or quaternion rotations tomorrow.
pub(super) fn weighted_distance(deltas: &[(f64, f64)]) -> f64 {
  deltas
    .iter()
    .map(|(d, w)| {
      let scaled = d * w;
      scaled * scaled
    })
    .sum::<f64>()
    .sqrt()
}

pub(super) fn euclidean_distance(row: f64, col: f64) -> f64 {
  weighted_distance(&[
    (row - ANCHOR_ROW, 1.0),
    ((col - ANCHOR_COL), CHAR_ASPECT_RATIO),
  ])
}

pub(super) fn core_distance(row: f64, col: f64) -> f64 {
  weighted_distance(&[
    (row - (ANCHOR_ROW + CORE_ROW_OFFSET), 1.0),
    ((col - ANCHOR_COL), CHAR_ASPECT_RATIO),
  ])
}

pub(super) fn core_total_ticks() -> usize {
  core_grow_ticks() + CORE_HOLD_TICKS
}

pub(super) fn core_grow_ticks() -> usize {
  (CORE_MAX_RADIUS / CORE_GROW_SPEED).ceil() as usize
}

pub(super) fn core_radius(tick: usize) -> f64 {
  let grow_ticks = core_grow_ticks();
  if tick < grow_ticks {
    tick as f64 * CORE_GROW_SPEED
  } else {
    CORE_MAX_RADIUS
  }
}

#[cfg(test)]
mod tests {
  use super::*;

  fn sample_lines() -> Vec<&'static str> {
    vec!["  ▓▓▓  ", " ▓▓▓▓▓ ", "▓▓▓▓▓▓▓", " ▓▓▓▓▓ ", "  ▓▓▓  "]
  }

  #[test]
  fn distance_map_assigns_infinity_to_spaces() {
    let lines = sample_lines();
    let map = build_distance_map(&lines);

    assert_eq!(distance_at(&map, 0, 0), f64::INFINITY);
    assert_eq!(distance_at(&map, 0, 1), f64::INFINITY);
  }

  #[test]
  fn distance_map_computes_finite_for_visible_chars() {
    let lines = sample_lines();
    let map = build_distance_map(&lines);

    assert!(distance_at(&map, 2, 3).is_finite());
    assert!(map.max_distance > 0.0);
  }

  #[test]
  fn euclidean_distance_at_anchor_is_zero() {
    let d = euclidean_distance(ANCHOR_ROW, ANCHOR_COL);
    assert!(d < 0.01);
  }

  #[test]
  fn core_distance_is_finite_for_all_positions() {
    assert!(core_distance(0.0, 0.0).is_finite());
    assert!(core_distance(10.0, 19.0).is_finite());
  }

  #[test]
  fn core_radius_grows_then_caps() {
    assert_eq!(core_radius(0), 0.0);
    let grow = core_grow_ticks();
    assert!((core_radius(grow) - CORE_MAX_RADIUS).abs() < 0.01);
    assert!((core_radius(grow + 100) - CORE_MAX_RADIUS).abs() < 0.01);
  }

  #[test]
  fn core_total_ticks_is_sum_of_grow_and_hold() {
    let total = core_total_ticks();
    assert_eq!(total, core_grow_ticks() + CORE_HOLD_TICKS);
  }
}
