///|
/// The instantaneous direction of the tide.
pub enum TideState {
  Rising
  Falling
  HighSlack
  LowSlack
} derive(Eq, Debug)

///|
/// One sampled point on a tide curve.
pub struct TideSample {
  jd : Double
  height : Double
  rate : Double
  state : TideState
} derive(Debug)

///|
/// Aggregate information about a sampled tide series.
pub struct TideStats {
  min_height : Double
  max_height : Double
  mean_height : Double
  range : Double
  max_flood_rate : Double
  max_ebb_rate : Double
  rising_fraction : Double
} derive(Debug)

///|
/// A closed interval on the tide timeline.
pub struct TideWindow {
  start_jd : Double
  end_jd : Double
  label : String
} derive(Debug)

///|
/// Approximate rate of change in meters per hour.
pub fn tide_rate(
  station : Station,
  jd : Double,
  delta_hours : Double,
) -> Double {
  let delta = if delta_hours <= 0.0 { 0.05 } else { delta_hours }
  let before = tide_height(station, jd - delta / 24.0)
  let after = tide_height(station, jd + delta / 24.0)
  (after - before) / (2.0 * delta)
}

///|
/// Classify a tide point by its local rate and height.
pub fn tide_state(
  station : Station,
  jd : Double,
  tolerance : Double,
) -> TideState {
  let rate = tide_rate(station, jd, 0.05)
  if rate > tolerance {
    Rising
  } else if rate < -tolerance {
    Falling
  } else {
    let height = tide_height(station, jd)
    if height >= station.datum {
      HighSlack
    } else {
      LowSlack
    }
  }
}

///|
/// Sample a station over a time window.
pub fn sample_series(
  station : Station,
  start_jd : Double,
  duration_hours : Double,
  step_hours : Double,
) -> Array[TideSample] {
  let step = if step_hours <= 0.0 { 1.0 } else { step_hours }
  let count = @math.floor(duration_hours / step).to_int() + 1
  let samples = Array(capacity=count)
  for i = 0; i < count; i = i + 1 {
    let jd = start_jd + i.to_double() * step / 24.0
    samples.push({
      jd,
      height: tide_height(station, jd),
      rate: tide_rate(station, jd, 0.05),
      state: tide_state(station, jd, 0.005),
    })
  }
  samples
}

///|
/// Compute descriptive statistics from a sampled series.
pub fn compute_stats(samples : Array[TideSample]) -> TideStats {
  if samples.is_empty() {
    return {
      min_height: 0.0,
      max_height: 0.0,
      mean_height: 0.0,
      range: 0.0,
      max_flood_rate: 0.0,
      max_ebb_rate: 0.0,
      rising_fraction: 0.0,
    }
  }
  let mut min_height = samples[0].height
  let mut max_height = samples[0].height
  let mut sum = 0.0
  let mut max_flood = 0.0
  let mut max_ebb = 0.0
  let mut rising = 0
  for sample in samples {
    if sample.height < min_height {
      min_height = sample.height
    }
    if sample.height > max_height {
      max_height = sample.height
    }
    sum = sum + sample.height
    if sample.rate > max_flood {
      max_flood = sample.rate
    }
    if sample.rate < max_ebb {
      max_ebb = sample.rate
    }
    if sample.state == TideState::Rising {
      rising = rising + 1
    }
  }
  {
    min_height,
    max_height,
    mean_height: sum / samples.length().to_double(),
    range: max_height - min_height,
    max_flood_rate: max_flood,
    max_ebb_rate: max_ebb,
    rising_fraction: rising.to_double() / samples.length().to_double(),
  }
}

///|
/// Find the next high or low water after `jd`.
pub fn find_next_extreme(
  station : Station,
  jd : Double,
  horizon_hours : Double,
  step_hours : Double,
) -> TideExtreme? {
  let step = if step_hours <= 0.0 { 0.1 } else { step_hours }
  let extremes = find_extremes(station, jd, horizon_hours, step)
  if extremes.is_empty() {
    None
  } else {
    Some(extremes[0])
  }
}

///|
/// Build a time window around an extreme.
pub fn window_around(
  extreme : TideExtreme,
  before_hours : Double,
  after_hours : Double,
  label : String,
) -> TideWindow {
  {
    start_jd: extreme.time_jd - before_hours / 24.0,
    end_jd: extreme.time_jd + after_hours / 24.0,
    label,
  }
}

///|
/// Test whether a window contains a Julian day.
pub fn window_contains(window : TideWindow, jd : Double) -> Bool {
  jd >= window.start_jd && jd <= window.end_jd
}

///|
/// Merge overlapping or touching windows.
pub fn merge_windows(windows : Array[TideWindow]) -> Array[TideWindow] {
  if windows.is_empty() {
    return []
  }
  let sorted = windows.copy()
  sorted.sort_by(fn(a, b) {
    if a.start_jd < b.start_jd {
      -1
    } else if a.start_jd > b.start_jd {
      1
    } else {
      0
    }
  })
  let merged = Array(capacity=sorted.length())
  let first = sorted[0]
  merged.push(first)
  for i = 1; i < sorted.length(); i = i + 1 {
    let current = sorted[i]
    let last = merged[merged.length() - 1]
    if current.start_jd <= last.end_jd {
      if current.end_jd > last.end_jd {
        merged[merged.length() - 1] = {
          start_jd: last.start_jd,
          end_jd: current.end_jd,
          label: last.label,
        }
      }
    } else {
      merged.push(current)
    }
  }
  merged
}

///|
/// Produce windows around every low water in a series.
pub fn low_water_windows(
  extremes : Array[TideExtreme],
  before_hours : Double,
  after_hours : Double,
) -> Array[TideWindow] {
  let windows = Array(capacity=extremes.length())
  for extreme in extremes {
    if extreme.kind == ExtremeKind::Low {
      windows.push(
        window_around(extreme, before_hours, after_hours, "低潮窗口"),
      )
    }
  }
  merge_windows(windows)
}

///|
/// Produce windows around every high water in a series.
pub fn high_water_windows(
  extremes : Array[TideExtreme],
  before_hours : Double,
  after_hours : Double,
) -> Array[TideWindow] {
  let windows = Array(capacity=extremes.length())
  for extreme in extremes {
    if extreme.kind == ExtremeKind::High {
      windows.push(
        window_around(extreme, before_hours, after_hours, "高潮窗口"),
      )
    }
  }
  merge_windows(windows)
}

///|
/// Return the dominant state in a sampled series.
pub fn dominant_state(samples : Array[TideSample]) -> TideState {
  let mut rising = 0
  let mut falling = 0
  let mut high = 0
  let mut low = 0
  for sample in samples {
    match sample.state {
      TideState::Rising => rising = rising + 1
      TideState::Falling => falling = falling + 1
      TideState::HighSlack => high = high + 1
      TideState::LowSlack => low = low + 1
    }
  }
  if rising >= falling && rising >= high && rising >= low {
    Rising
  } else if falling >= high && falling >= low {
    Falling
  } else if high >= low {
    HighSlack
  } else {
    LowSlack
  }
}

///|
/// Convert the dominant state to a human-readable label.
pub fn state_label(state : TideState) -> String {
  match state {
    TideState::Rising => "涨潮"
    TideState::Falling => "落潮"
    TideState::HighSlack => "高潮平潮"
    TideState::LowSlack => "低潮平潮"
  }
}

///|
/// Compute the tide coefficient using the range relative to the mean range.
pub fn tide_coefficient(tidal_range : Double, mean_range : Double) -> Double {
  if mean_range <= 0.0 {
    0.0
  } else {
    tidal_range / mean_range * 100.0
  }
}

///|
/// Return true when a range is above the spring threshold.
pub fn is_spring(tidal_range : Double, mean_range : Double) -> Bool {
  tide_coefficient(tidal_range, mean_range) >= 110.0
}

///|
/// Return true when a range is below the neap threshold.
pub fn is_neap(tidal_range : Double, mean_range : Double) -> Bool {
  tide_coefficient(tidal_range, mean_range) <= 70.0
}