///|
/// A recoverable validation error; no user input is inserted as raw XML.
#derive(Debug)
pub suberror OdsError {
  Invalid(String)
}

///|
/// Formula cache values have the same types as literal values.
/// Date is a Gregorian ODF date in 1..9999, independent of an application's
/// serial date epoch. It does not promise Excel supports historical dates.
#derive(Eq, Debug)
pub(all) enum Value {
  Empty
  Text(String)
  Number(Double)
  Percentage(Double)
  Currency(Double, String)
  Duration(String)
  DateTime(String)
  Boolean(Bool)
  Date(Int32, Int32, Int32)
}

///|
struct Cell {
  value : Value
  formula : String?
  style : Style
}

///|
/// Validate ODF values; application-specific date ranges are not inferred.
pub fn Cell::new(value : Value) -> Cell raise OdsError {
  validate_value(value)
  { value, formula: None, style: Plain, }
}

///|
/// Optional date policy for Excel's 1900 date system, with an ISO date style.
/// Rejects dates before 1900-01-01 and all invalid Gregorian dates, including
/// Excel's fictional 1900-02-29. Does not configure a reader's date system or
/// guarantee rendering. For historical display, callers may explicitly use
/// Text("0001-01-01"), which has no date arithmetic semantics.
pub fn Cell::excel_1900_date(
  year : Int32,
  month : Int32,
  day : Int32,
) -> Cell raise OdsError {
  let cell = Cell::new(Date(year, month, day))
  if year < 1900 {
    raise Invalid("Excel 1900 date policy requires 1900-01-01 or later")
  }
  cell.styled(DateISO)
}

///|
/// OpenFormula text must begin of:=. This validates storage, not formula syntax.
/// The cache is supplied by the caller and may be stale. Neither editing other
/// cells nor serialization recalculates it; application open/save may retain it.
/// Formula text is stored verbatim, including possible external references.
/// Only pass trusted expressions and manage cache freshness explicitly.
/// Date caches default to DateISO so readers keep the date presentation when
/// evaluating DATE formulas. Callers may still explicitly override the style.
pub fn Cell::formula(expression : String, cache : Value) -> Cell raise OdsError {
  validate_text(expression)
  if !expression.has_prefix("of:=") || expression.length() <= 4 {
    raise Invalid("formula must be nonempty OpenFormula text beginning of:=")
  }
  validate_value(cache)
  if cache == Empty {
    raise Invalid("formula requires a typed nonempty cache")
  }
  let style = match cache {
    Date(_, _, _) => DateISO
    _ => Plain
  }
  { value: cache, formula: Some(expression), style, }
}

///|
/// Returns the stored literal value or caller/imported formula cache. This does
/// not evaluate the formula or assert that its cache matches current operands.
pub fn Cell::value(self : Cell) -> Value {
  self.value
}

///|
pub fn Cell::expression(self : Cell) -> String? {
  self.formula
}

///|
fn validate_text(s : String) -> Unit raise OdsError {
  if s.length() > 32767 {
    raise Invalid("text exceeds 32767 UTF-16 units")
  }
  for c in s {
    let n = c.to_int()
    if !(n == 9 ||
      n == 10 ||
      n == 13 ||
      (n >= 32 && n <= 0xD7FF) ||
      (n >= 0xE000 && n <= 0xFFFD) ||
      (n >= 0x10000 && n <= 0x10FFFF)) {
      raise Invalid("text contains an XML 1.0 forbidden character")
    }
  }
}

///|
fn validate_value(v : Value) -> Unit raise OdsError {
  match v {
    Empty | Boolean(_) => ()
    Text(s) => validate_text(s)
    Percentage(n) | Number(n) =>
      if n.is_nan() || n.is_inf() {
        raise Invalid("number must be finite")
      }
    Currency(n, code) => {
      if n.is_nan() || n.is_inf() {
        raise Invalid("currency amount must be finite")
      }
      if code.length() != 3 {
        raise Invalid("currency requires three uppercase ISO-style letters")
      }
      for c in code {
        if c < 'A' || c > 'Z' {
          raise Invalid("currency requires three uppercase ISO-style letters")
        }
      }
    }
    Duration(text) => validate_duration(text)
    DateTime(text) => validate_datetime(text)
    Date(y, m, d) => {
      if y < 1 || y > 9999 || m < 1 || m > 12 {
        raise Invalid("date is outside Gregorian range")
      }
      let leap = y % 4 == 0 && (y % 100 != 0 || y % 400 == 0)
      let days = match m {
        2 => if leap { 29 } else { 28 }
        4 | 6 | 9 | 11 => 30
        _ => 31
      }
      if d < 1 || d > days {
        raise Invalid("invalid Gregorian day")
      }
    }
  }
}

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

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

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

///|
fn extended_digits(
  chars : Array[Char],
  start : Int32,
  count : Int32,
) -> Int32 raise OdsError {
  if start < 0 || count < 1 || count > 9 || start + count > chars.length() {
    raise Invalid("invalid temporal digits")
  }
  let mut n = 0
  for i in start..<(start + count) {
    let c = chars[i]
    if c < '0' || c > '9' {
      raise Invalid("invalid temporal digits")
    }
    n = n * 10 + c.to_int() - 48
  }
  n
}

///|
fn extended_chars(text : String) -> Array[Char] raise OdsError {
  if text.length() > 128 {
    raise Invalid("temporal lexical value exceeds 128 units")
  }
  let chars = []
  for c in text {
    chars.push(c)
  }
  chars
}

///|
// A Gregorian local or offset timestamp. No timezone conversion or arithmetic;
// the exact lexical offset/fraction is stored. Leap seconds/year zero excluded.
fn validate_datetime(text : String) -> Unit raise OdsError {
  let c = extended_chars(text)
  if c.length() < 19 ||
    c[4] != '-' ||
    c[7] != '-' ||
    c[10] != 'T' ||
    c[13] != ':' ||
    c[16] != ':' {
    raise Invalid(
      "dateTime requires YYYY-MM-DDThh:mm:ss with optional fraction/offset",
    )
  }
  validate_value(
    Date(
      extended_digits(c, 0, 4),
      extended_digits(c, 5, 2),
      extended_digits(c, 8, 2),
    ),
  )
  if extended_digits(c, 11, 2) > 23 ||
    extended_digits(c, 14, 2) > 59 ||
    extended_digits(c, 17, 2) > 59 {
    raise Invalid("dateTime clock fields outside supported range")
  }
  let mut at = 19
  if at < c.length() && c[at] == '.' {
    at += 1
    let start = at
    while at < c.length() && c[at] >= '0' && c[at] <= '9' {
      at += 1
    }
    if at - start < 1 || at - start > 9 {
      raise Invalid("dateTime fraction requires 1..9 digits")
    }
  }
  if at == c.length() {
    return
  }
  if c[at] == 'Z' && at + 1 == c.length() {
    return
  }
  if c.length() - at != 6 || (c[at] != '+' && c[at] != '-') || c[at + 3] != ':' {
    raise Invalid("invalid dateTime UTC offset")
  }
  let hour = extended_digits(c, at + 1, 2)
  let minute = extended_digits(c, at + 4, 2)
  if hour > 14 || minute > 59 || (hour == 14 && minute != 0) {
    raise Invalid("dateTime UTC offset outside 14:00")
  }
}

///|
// Day/time duration subset of xsd:duration. Never treats calendar months/years
// as a fixed number of seconds; preserve exact sign/components/fraction.
fn validate_duration(text : String) -> Unit raise OdsError {
  let c = extended_chars(text)
  let mut at = if !c.is_empty() && c[0] == '-' { 1 } else { 0 }
  if at >= c.length() || c[at] != 'P' {
    raise Invalid("duration requires ISO day/time P...T... form")
  }
  at += 1
  let mut time = false
  let mut time_parts = 0
  let mut parts = 0
  let mut order = -1
  while at < c.length() {
    if c[at] == 'T' {
      if time {
        raise Invalid("duplicate duration T separator")
      }
      time = true
      at += 1
      continue
    }
    let start = at
    while at < c.length() && c[at] >= '0' && c[at] <= '9' {
      at += 1
    }
    if at - start < 1 || at - start > 9 {
      raise Invalid("duration component requires 1..9 digits")
    }
    let mut fraction = false
    if at < c.length() && c[at] == '.' {
      fraction = true
      at += 1
      let start = at
      while at < c.length() && c[at] >= '0' && c[at] <= '9' {
        at += 1
      }
      if at - start < 1 || at - start > 9 {
        raise Invalid("duration fraction requires 1..9 digits")
      }
    }
    if at >= c.length() {
      raise Invalid("duration component missing unit")
    }
    let next = match c[at] {
      'D' if !time && !fraction => 0
      'H' if time && !fraction => 1
      'M' if time && !fraction => 2
      'S' if time => 3
      _ => raise Invalid("unsupported duration unit or fraction")
    }
    if next <= order {
      raise Invalid("duration units duplicated or out of order")
    }
    order = next
    at += 1
    parts += 1
    if time {
      time_parts += 1
    }
  }
  if parts == 0 || (time && time_parts == 0) {
    raise Invalid("empty or incomplete duration")
  }
}