///|
/// An Ion value that has no PartiQL equivalent.
pub(all) suberror LowerError {
  /// A symbol whose text is unknown (a symbol ID that no table defines).
  SymbolWithoutText
} derive(Debug, Eq)

///|
/// Converts an Ion timestamp: missing fields become 0, an unknown offset
/// becomes UTC, and more than 9 fractional digits are truncated.
pub fn timestamp_of_ion(ts : @ion_core.Timestamp) -> Timestamp {
  let (nanos, precision) = match ts.fraction {
    None => (0, 0)
    Some(fraction) => {
      // Int64: `-exponent` must not overflow.
      let digits = -fraction.exponent.to_int64()
      if digits <= 0L {
        (0, 0)
      } else if digits <= 9L {
        let d = digits.to_int()
        let scale = @bigint.BigInt::from_int(pow10(9 - d))
        ((fraction.coefficient * scale).to_int(), d)
      } else {
        // Dropping more digits than the coefficient has leaves 0; never
        // build a power of ten larger than the coefficient.
        let drop = digits - 9L
        if drop > digit_count(fraction.coefficient).to_int64() {
          (0, 9)
        } else {
          let scale = 10N.pow(@bigint.BigInt::from_int64(drop))
          ((fraction.coefficient / scale).to_int(), 9)
        }
      }
    }
  }
  let offset = match ts.offset {
    Some(Known(minutes)) => minutes
    _ => 0
  }
  // An Ion timestamp is always valid, so these constructors do not raise.
  let date = Date::{
    year: ts.year,
    month: ts.month.unwrap_or(1),
    day: ts.day.unwrap_or(1),
  }
  let time = Time::{
    hour: ts.hour.unwrap_or(0),
    minute: ts.minute.unwrap_or(0),
    second: ts.second.unwrap_or(0),
    nanos,
    precision,
    offset: Some(offset),
  }
  { date, time, }
}

///|
fn lower_ion(x : @ion_core.IonValue) -> Value raise LowerError {
  match x.kind() {
    Null(_) => Null
    Bool(b) => Bool(b)
    Int(n) => Value::integer(n)
    Float(d) => Float(d)
    Decimal(d) => Decimal(d)
    Timestamp(ts) => Timestamp(timestamp_of_ion(ts))
    Symbol(token) =>
      match token.text() {
        Some(text) => String(text)
        None => raise SymbolWithoutText
      }
    String(s) => String(s)
    Clob(bytes) => Clob(bytes)
    Blob(bytes) => Blob(bytes)
    List(items) | Sexp(items) => List(items.map(item => Ion(item)))
    Struct(fields) => {
      let pairs : Array[(String, Value)] = []
      for field in fields {
        match field.name().text() {
          Some(name) => pairs.push((name, Ion(field.value())))
          None => raise SymbolWithoutText
        }
      }
      Tuple(Tuple::new(pairs))
    }
  }
}

///|
/// Converts a boxed Ion value to its core value, one level deep. Other
/// values are returned unchanged.
pub fn Value::lower(self : Value) -> Value raise LowerError {
  match self {
    Ion(x) => lower_ion(x)
    other => other
  }
}

///|
/// Lowers recursively, including the children of collections and tuples.
pub fn Value::lower_deep(self : Value) -> Value raise LowerError {
  match self.lower() {
    List(items) => {
      let out : Array[Value] = []
      for item in items {
        out.push(item.lower_deep())
      }
      List(out)
    }
    Bag(bag) => {
      let out : Array[Value] = []
      for item in bag.items() {
        out.push(item.lower_deep())
      }
      Bag(Bag::new(out))
    }
    Tuple(tuple) => {
      let out : Array[(String, Value)] = []
      for pair in tuple.fields() {
        out.push((pair.0, pair.1.lower_deep()))
      }
      Tuple(Tuple::new(out))
    }
    // Keys are stored lowered already; values may hold boxed Ion.
    Map(m) => {
      let out : Array[(Value, Value)] = []
      for pair in m.entries() {
        out.push((pair.0, pair.1.lower_deep()))
      }
      Map({ key_type: m.key_type, value_type: m.value_type, entries: out, })
    }
    Graph(g) => {
      let nodes : Array[GraphNode] = []
      for n in g.nodes() {
        nodes.push({ ..n, payload: lower_payload(n.payload), })
      }
      let edges : Array[GraphEdge] = []
      for e in g.edges() {
        edges.push({ ..e, payload: lower_payload(e.payload), })
      }
      Graph({ nodes, edges, })
    }
    other => other
  }
}

///|
fn lower_payload(payload : Value?) -> Value? raise LowerError {
  match payload {
    Some(v) => Some(v.lower_deep())
    None => None
  }
}