///|
fn annotate(x : @ion_core.IonValue, name : String) -> @ion_core.IonValue {
  x.with_annotations(
    [@ion_core.SymbolToken::SymbolToken(name), ..x.annotations()],
  )
}

///|
fn int64(n : Int64) -> @ion_core.IonValue {
  @ion_core.IonValue::big_int(@bigint.BigInt::from_int64(n))
}

///|
fn small(n : Int32) -> @ion_core.IonValue {
  @ion_core.IonValue::int(n)
}

///|
fn seconds_decimal(t : @value.Time) -> @ion_core.IonValue {
  let unit = 10N.pow(@bigint.BigInt::from_int(t.precision))
  let fraction = @bigint.BigInt::from_int(t.nanos) /
    10N.pow(@bigint.BigInt::from_int(9 - t.precision))
  let coefficient = @bigint.BigInt::from_int(t.second) * unit + fraction
  @ion_core.IonValue::decimal(
    @ion_core.Decimal::Decimal(coefficient, -t.precision),
  )
}

///|
fn offset_value(offset : Int32?) -> @ion_core.IonValue {
  match offset {
    Some(minutes) => small(minutes)
    None => @ion_core.IonValue::untyped_null()
  }
}

///|
fn struct_of(
  fields : Array[(String, @ion_core.IonValue)],
) -> @ion_core.IonValue {
  @ion_core.IonValue::from_fields(
    fields.map(pair => @ion_core.Field::Field(pair.0, pair.1)),
  )
}

///|
fn sign_value(negative : Bool) -> @ion_core.IonValue {
  @ion_core.IonValue::string(if negative { "-" } else { "+" })
}

///|
/// Encodes a value in PartiQL-encoded Ion. A boxed Ion value is written as
/// `$ion::x`.
pub fn to_ion(v : @value.Value) -> @ion_core.IonValue {
  encode(v, false)
}

///|
/// Encodes a value for Ion data (the Ion-preserving mode): a boxed Ion
/// value, at any depth, is written exactly as it was read; other values use
/// the PartiQL-encoded forms. Read the output with `from_ion_data`, not
/// `from_ion`: a boxed value whose own annotation is, for example, `$bag`
/// is written as is, so `from_ion` would read it as a PartiQL bag.
pub fn to_ion_data(v : @value.Value) -> @ion_core.IonValue {
  encode(v, true)
}

///|
/// Decodes Ion data in the Ion-preserving mode: the value is boxed, so every
/// Ion detail is kept until a semantic lowers it (ADR 0005).
pub fn from_ion_data(x : @ion_core.IonValue) -> @value.Value {
  @value.Value::from_ion(x)
}

///|
/// The shared encoder. `preserve` writes boxed Ion values exactly.
fn encode(v : @value.Value, preserve : Bool) -> @ion_core.IonValue {
  v.fold((v, children) => {
    match v {
      Null => @ion_core.IonValue::untyped_null()
      Missing => annotate(@ion_core.IonValue::untyped_null(), "$missing")
      Bool(b) => @ion_core.IonValue::bool(b)
      Int(n) => int64(n)
      BigInt(n) => @ion_core.IonValue::big_int(n)
      Float(d) => @ion_core.IonValue::float(d)
      Decimal(d) => @ion_core.IonValue::decimal(d)
      String(s) => @ion_core.IonValue::string(s)
      Clob(bytes) => @ion_core.IonValue::clob(bytes)
      Blob(bytes) => @ion_core.IonValue::blob(bytes)
      Date(d) =>
        annotate(
          struct_of([
            ("year", small(d.year)),
            ("month", small(d.month)),
            ("day", small(d.day)),
          ]),
          "$date",
        )
      Time(t) =>
        annotate(
          struct_of([
            ("hour", small(t.hour)),
            ("minute", small(t.minute)),
            ("second", seconds_decimal(t)),
            ("offset", offset_value(t.offset)),
          ]),
          "$time",
        )
      Timestamp(ts) =>
        annotate(
          struct_of([
            ("year", small(ts.date.year)),
            ("month", small(ts.date.month)),
            ("day", small(ts.date.day)),
            ("hour", small(ts.time.hour)),
            ("minute", small(ts.time.minute)),
            ("second", seconds_decimal(ts.time)),
            ("offset", offset_value(ts.time.offset)),
          ]),
          "$timestamp",
        )
      IntervalYM(i) =>
        annotate(
          struct_of([
            ("sign", sign_value(i.is_negative())),
            ("years", int64(i.years())),
            ("months", int64(i.months_part())),
          ]),
          "$interval_ym",
        )
      IntervalDT(i) =>
        annotate(
          struct_of([
            ("sign", sign_value(i.is_negative())),
            ("days", int64(i.days())),
            ("hours", int64(i.hours())),
            ("minutes", int64(i.minutes())),
            ("seconds", int64(i.seconds_part())),
            ("nanos", int64(i.nanos_part())),
          ]),
          "$interval_dt",
        )
      List(_) => @ion_core.IonValue::list(children)
      Bag(_) => annotate(@ion_core.IonValue::list(children), "$bag")
      Tuple(t) => struct_of(t.fields().mapi((i, p) => (p.0, children[i])))
      Map(m) => {
        let pairs = m
          .entries()
          .mapi((i, _) => {
            @ion_core.IonValue::list([children[2 * i], children[2 * i + 1]])
          })
        @ion_core.IonValue::list(pairs).with_annotations([
          @ion_core.SymbolToken::SymbolToken("$map"),
          @ion_core.SymbolToken::SymbolToken(m.key_type.name()),
          @ion_core.SymbolToken::SymbolToken(m.value_type.name()),
        ])
      }
      Graph(g) => encode_graph(g, children)
      Ion(x) => if preserve { x } else { annotate(x, "$ion") }
    }
  })
}

///|
fn symbol(text : String) -> @ion_core.IonValue {
  @ion_core.IonValue::symbol(text)
}

///|
/// The fields shared by nodes and edges: labels when there are any, and the
/// payload when there is one.
fn element_fields(
  labels : Array[String],
  payload : @ion_core.IonValue?,
) -> Array[(String, @ion_core.IonValue)] {
  let out : Array[(String, @ion_core.IonValue)] = []
  if !labels.is_empty() {
    out.push(
      (
        "labels",
        @ion_core.IonValue::list(labels.map(@ion_core.IonValue::string)),
      ),
    )
  }
  if payload is Some(p) {
    out.push(("payload", p))
  }
  out
}

///|
/// `$graph::{nodes: [...], edges: [...]}` (the external graph format).
fn encode_graph(
  g : @value.GraphValue,
  children : Array[@ion_core.IonValue],
) -> @ion_core.IonValue {
  let mut at = 0
  let payload = (p : @value.Value?) => {
    match p {
      None => None
      Some(_) => {
        let x = children[at]
        at += 1
        Some(x)
      }
    }
  }
  let nodes = g
    .nodes()
    .iter()
    .map(n => {
      struct_of(
        [
          ("id", symbol(n.id)),
          ..element_fields(n.labels().to_owned(), payload(n.payload)),
        ],
      )
    })
    .collect()
  let edges = g
    .edges()
    .iter()
    .map(e => {
      let ends = match e.ends {
        Directed(from~, to~) =>
          @ion_core.IonValue::sexp([symbol(from), symbol("->"), symbol(to)])
        Undirected(a, b) =>
          @ion_core.IonValue::sexp([symbol(a), symbol("--"), symbol(b)])
      }
      struct_of(
        [
          ("id", symbol(e.id)),
          ..element_fields(e.labels().to_owned(), payload(e.payload)),
          ("ends", ends),
        ],
      )
    })
    .collect()
  annotate(
    struct_of([
      ("nodes", @ion_core.IonValue::list(nodes)),
      ("edges", @ion_core.IonValue::list(edges)),
    ]),
    "$graph",
  )
}