// Numeric detection for cell values, so `set`/`csv` store numbers as real
// numeric cells (making formulas evaluate and number formats render in Excel)
// while leaving anything ambiguous as text.

///|
enum CellKind {
  AsInt(Int32)
  AsFloat(Double)
  AsText
}

///|
/// Whether `s` is a plain, canonical number: an optional leading `-`, an
/// integer part with no redundant leading zero (`0`, or `1`-`9` then digits),
/// and an optional `.` followed by at least one digit. This deliberately
/// rejects things a spreadsheet should keep as text — leading zeros (`007`),
/// a leading `+`, exponent forms (`1e3`), thousands separators, and
/// whitespace — so identifiers that merely look numeric aren't coerced.
fn is_plain_number(s : String) -> Bool {
  let chars = []
  for c in s {
    chars.push(c)
  }
  let n = chars.length()
  let mut i = 0
  if i < n && chars[i] == '-' {
    i = i + 1
  }
  // Integer part.
  if i >= n {
    return false
  }
  if chars[i] == '0' {
    i = i + 1
  } else if chars[i] >= '1' && chars[i] <= '9' {
    i = i + 1
    while i < n && chars[i] >= '0' && chars[i] <= '9' {
      i = i + 1
    }
  } else {
    return false
  }
  // Optional fractional part.
  if i < n && chars[i] == '.' {
    i = i + 1
    let frac_start = i
    while i < n && chars[i] >= '0' && chars[i] <= '9' {
      i = i + 1
    }
    if i == frac_start {
      return false
    }
  }
  i == n
}

///|
/// Classify a raw cell string as an integer, a float, or text. A value is only
/// treated as a number when it is a plain canonical number (`is_plain_number`);
/// an integer that round-trips exactly through `Int` is an `AsInt`, a larger or
/// fractional number is an `AsFloat`, and everything else stays text.
fn classify_cell_value(raw : String) -> CellKind {
  if !is_plain_number(raw) {
    return AsText
  }
  // Canonical integer that fits in `Int` (round-trip rejects overflow, where
  // parsing would fail or wrap).
  let as_int = Some(@string.parse_int(raw)) catch { _ => None }
  match as_int {
    Some(value) => if value.to_string() == raw { return AsInt(value) }
    None => ()
  }
  match (Some(@string.parse_double(raw)) catch { _ => None }) {
    Some(value) => AsFloat(value)
    None => AsText
  }
}

// Tests for numeric cell-value detection.

///|
/// Render a `CellKind` compactly for assertions (exhaustive match — a new
/// variant would fail to compile here rather than silently pass).
fn kind_str(kind : CellKind) -> String {
  match kind {
    AsInt(value) => "int:\{value}"
    AsFloat(value) => "float:\{value}"
    AsText => "text"
  }
}

///|
test "classify_cell_value: integers" {
  inspect(kind_str(classify_cell_value("42")), content="int:42")
  inspect(kind_str(classify_cell_value("0")), content="int:0")
  inspect(kind_str(classify_cell_value("-5")), content="int:-5")
  inspect(kind_str(classify_cell_value("1000000")), content="int:1000000")
}

///|
test "classify_cell_value: floats" {
  inspect(kind_str(classify_cell_value("9.99")), content="float:9.99")
  inspect(kind_str(classify_cell_value("-3.5")), content="float:-3.5")
  inspect(kind_str(classify_cell_value("0.5")), content="float:0.5")
  // An integer too large for Int becomes a float rather than text.
  inspect(
    kind_str(classify_cell_value("3000000000")),
    content="float:3000000000",
  )
}

///|
test "classify_cell_value: kept as text" {
  // Leading zero (zip codes, ids), leading '+', exponent, separators, and
  // non-numeric strings must all stay text.
  inspect(kind_str(classify_cell_value("007")), content="text")
  inspect(kind_str(classify_cell_value("+5")), content="text")
  inspect(kind_str(classify_cell_value("1e3")), content="text")
  inspect(kind_str(classify_cell_value("1,000")), content="text")
  inspect(kind_str(classify_cell_value("")), content="text")
  inspect(kind_str(classify_cell_value("9.99.9")), content="text")
  inspect(kind_str(classify_cell_value("9.")), content="text")
  inspect(kind_str(classify_cell_value("-")), content="text")
  inspect(kind_str(classify_cell_value("Widget")), content="text")
  inspect(kind_str(classify_cell_value("  10")), content="text")
}