///|
pub(all) enum DateFrequency {
  Year
  Month
  Day
  Hour
  Minute
  Second
  Microsecond
} derive(Debug, Eq, Compare)

///|
pub(all) enum DateMaxTicks {
  All(Int32)
  ByFrequency(Array[(DateFrequency, Int32)])
} derive(Debug, Eq)

///|
pub(all) enum AutoDateWarning {
  IntervalFallback(DateFrequency, Int32)
  DistantMicroseconds
} derive(Debug, Eq)

///|
priv struct AutoLocatorState {
  mut frequency : DateFrequency
  mut interval : Int32
  mut warnings : Array[AutoDateWarning]
}

///|
priv struct AutoLocatorSettings {
  minticks : Int32
  maxticks : Array[Int32]
  intervals : Array[Array[Int32]]
  multiples : Bool
  offset_minutes : Int32
  timezone : DateTimeZone?
  context : DateParserContext?
}

///|
pub struct AutoDateLocator {
  priv settings : AutoLocatorSettings
  priv state : AutoLocatorState
}

///|
fn DateFrequency::index(self : DateFrequency) -> Int32 {
  match self {
    Year => 0
    Month => 1
    Day => 2
    Hour => 3
    Minute => 4
    Second => 5
    Microsecond => 6
  }
}

///|
fn auto_frequency(index : Int32) -> DateFrequency {
  match index {
    0 => Year
    1 => Month
    2 => Day
    3 => Hour
    4 => Minute
    5 => Second
    _ => Microsecond
  }
}

///|
fn auto_unit(frequency : DateFrequency) -> Double {
  match frequency {
    Year => 365.0
    Month => 30.0
    Day => 1.0
    Hour => 1.0 / 24.0
    Minute => 1.0 / 1440.0
    Second => 1.0 / 86400.0
    Microsecond => 1.0 / 86_400_000_000.0
  }
}

///|
fn auto_maximums(
  current : Array[Int32],
  value : DateMaxTicks,
) -> Array[Int32] raise DateError {
  let result = current.copy()
  match value {
    All(count) => {
      if count < 1 {
        raise DateError::InvalidDate(message="maxticks must be positive")
      }
      for index in 0..<7 {
        result[index] = count
      }
    }
    ByFrequency(values) =>
      for entry in values {
        if entry.1 < 1 {
          raise DateError::InvalidDate(message="maxticks must be positive")
        }
        result[entry.0.index()] = entry.1
      }
  }
  result
}

///|
/// Construct the upstream calendar-aware automatic selector. Configuration is
/// copied; selection updates frequency/interval/warnings independently of it.
/// context supplies host-local now for the year-multiple branch only.
///
/// ```mbt check
/// test {
///   let locator = @date.AutoDateLocator::new(maxticks=@date.All(5))
///   let first = @date.datetime(2024, 1, 1).to_ordinal()
///   let last = @date.datetime(2024, 6, 1).to_ordinal()
///   assert_true(!locator.tick_values(first, last).is_empty())
///   assert_eq(locator.frequency(), @date.Month)
/// }
/// ```
/// Capture an explicit timezone or the current timezone rc at construction.
pub fn AutoDateLocator::new(
  context? : DateParserContext,
  tz? : String,
  timezone? : DateTimeZone,
  minticks~ : Int32 = 5,
  maxticks? : DateMaxTicks,
  interval_multiples~ : Bool = true,
) -> AutoDateLocator raise DateError {
  if minticks < 1 {
    raise DateError::InvalidDate(message="minticks must be positive")
  }
  let zone = selected_timezone(tz, timezone)
  let defaults = [11, 12, 11, 12, 11, 11, 8]
  {
    settings: {
      context,
      minticks,
      maxticks: match maxticks {
        Some(value) => auto_maximums(defaults, value)
        None => defaults
      },
      intervals: [
        [
          1, 2, 4, 5, 10, 20, 40, 50, 100, 200, 400, 500, 1000, 2000, 4000, 5000,
          10000,
        ],
        [1, 2, 3, 4, 6],
        if interval_multiples {
          [1, 2, 4, 7, 14]
        } else {
          [1, 2, 3, 7, 14, 21]
        },
        [1, 2, 3, 4, 6, 12],
        [1, 5, 10, 15, 30],
        [1, 5, 10, 15, 30],
        [
          1, 2, 5, 10, 20, 50, 100, 200, 500, 1000, 2000, 5000, 10000, 20000, 50000,
          100000, 200000, 500000, 1000000,
        ],
      ],
      multiples: interval_multiples,
      offset_minutes: zone.initial_offset_minutes(),
      timezone: Some(zone),
    },
    state: { frequency: Year, interval: 1, warnings: [], },
  }
}

///|
/// Copy configuration and the last selection without sharing mutable state.
pub fn AutoDateLocator::copy(self : AutoDateLocator) -> AutoDateLocator {
  {
    settings: {
      ..self.settings,
      maxticks: self.settings.maxticks.copy(),
      intervals: self.settings.intervals.map(values => values.copy()),
    },
    state: {
      frequency: self.state.frequency,
      interval: self.state.interval,
      warnings: self.state.warnings.copy(),
    },
  }
}

///|
pub fn AutoDateLocator::minticks(self : AutoDateLocator) -> Int32 {
  self.settings.minticks
}

///|
pub fn AutoDateLocator::max_ticks(
  self : AutoDateLocator,
  frequency : DateFrequency,
) -> Int32 {
  self.settings.maxticks[frequency.index()]
}

///|
pub fn AutoDateLocator::maxticks(
  self : AutoDateLocator,
) -> Array[(DateFrequency, Int32)] {
  Array::makei(7, index => {
    (auto_frequency(index), self.settings.maxticks[index])
  })
}

///|
pub fn AutoDateLocator::intervals(
  self : AutoDateLocator,
  frequency : DateFrequency,
) -> Array[Int32] {
  self.settings.intervals[frequency.index()].copy()
}

///|
pub fn AutoDateLocator::interval_multiples(self : AutoDateLocator) -> Bool {
  self.settings.multiples
}

///|
pub fn AutoDateLocator::frequency(self : AutoDateLocator) -> DateFrequency {
  self.state.frequency
}

///|
pub fn AutoDateLocator::unit_scale(self : AutoDateLocator) -> Double {
  auto_unit(self.state.frequency)
}

///|
/// Return the last intervald candidate, before multiples become range selectors.
pub fn AutoDateLocator::selected_interval(self : AutoDateLocator) -> Int32 {
  self.state.interval
}

///|
/// Return diagnostics from the last selection, without printing warnings.
pub fn AutoDateLocator::warnings(
  self : AutoDateLocator,
) -> Array[AutoDateWarning] {
  self.state.warnings.copy()
}

///|
pub fn AutoDateLocator::with_minticks(
  self : AutoDateLocator,
  count : Int32,
) -> AutoDateLocator raise DateError {
  if count < 1 {
    raise DateError::InvalidDate(message="minticks must be positive")
  }
  let copy = self.copy()
  { ..copy, settings: { ..copy.settings, minticks: count, }, }
}

///|
pub fn AutoDateLocator::with_maxticks(
  self : AutoDateLocator,
  value : DateMaxTicks,
) -> AutoDateLocator raise DateError {
  let copy = self.copy()
  {
    ..copy,
    settings: {
      ..copy.settings,
      maxticks: auto_maximums(copy.settings.maxticks, value),
    },
  }
}

///|
/// Keep the given order, as upstream intervald does; empty/nonpositive lists
/// raise without changing this object. Other interval tables remain unchanged.
pub fn AutoDateLocator::with_intervals(
  self : AutoDateLocator,
  frequency : DateFrequency,
  values : Array[Int32],
) -> AutoDateLocator raise DateError {
  if values.is_empty() || values.any(value => value < 1) {
    raise DateError::InvalidDate(
      message="date intervals must be a nonempty positive list",
    )
  }
  let copy = self.copy()
  copy.settings.intervals[frequency.index()] = values.copy()
  copy
}

///|
/// Change alignment while retaining intervald, including any customization.
pub fn AutoDateLocator::with_interval_multiples(
  self : AutoDateLocator,
  enabled : Bool,
) -> AutoDateLocator {
  let copy = self.copy()
  { ..copy, settings: { ..copy.settings, multiples: enabled, }, }
}

///|
pub fn AutoDateLocator::with_offset(
  self : AutoDateLocator,
  offset_minutes : Int32,
) -> AutoDateLocator raise DateError {
  validate_offset(offset_minutes)
  let copy = self.copy()
  { ..copy, settings: { ..copy.settings, offset_minutes, timezone: None, }, }
}

///|
pub fn AutoDateLocator::with_timezone(
  self : AutoDateLocator,
  timezone : DateTimeZone,
) -> AutoDateLocator {
  let copy = self.copy()
  {
    ..copy,
    settings: {
      ..copy.settings,
      offset_minutes: timezone.initial_offset_minutes(),
      timezone: Some(timezone),
    },
  }
}

///|
/// Expand a single finite value by two years each side, or supply the numeric
/// 1970-01-01/02 range for nonfinite bounds. This does not select a frequency.
pub fn AutoDateLocator::nonsingular(
  _self : AutoDateLocator,
  lower : Double,
  upper : Double,
) -> (Double, Double) {
  if lower.is_nan() || upper.is_nan() || lower.is_inf() || upper.is_inf() {
    return (
      try! datetime(1970, 1, 1).to_ordinal(),
      try! datetime(1970, 1, 2).to_ordinal(),
    )
  }
  let first = Double::min(lower, upper)
  let last = Double::max(lower, upper)
  if first == last {
    (first - 730.0, last + 730.0)
  } else {
    (first, last)
  }
}

///|
pub extend DateFrequency with @debug.Debug::{to_repr}

///|
pub extend DateFrequency with Eq::{equal, not_equal}

///|
pub extend DateMaxTicks with @debug.Debug::{to_repr}

///|
pub extend DateMaxTicks with Eq::{equal, not_equal}

///|
pub extend AutoDateWarning with @debug.Debug::{to_repr}

///|
pub extend AutoDateWarning with Eq::{equal, not_equal}

///|
pub extend DateFrequency with Compare::{compare, op_lt, op_le, op_gt, op_ge}