///|
/// Expand a precompiled template. The binding array rejects duplicate names
/// so one name cannot silently change value during an expansion.
pub fn Template::expand(
  self : Template,
  bindings : Array[(String, Value)],
  max_output~ : Int32 = 1048576,
  max_bindings~ : Int32 = 4096,
  max_value_length~ : Int32 = 1048576,
  max_items~ : Int32 = 4096,
) -> Result[String, ExpandError] {
  match
    self.expand_internal(
      bindings, max_output, max_bindings, max_value_length, max_items, false,
    ) {
    Ok(result) => Ok(result.text)
    Err(error) => Err(error)
  }
}

///|
fn Template::expand_internal(
  self : Template,
  bindings : Array[(String, Value)],
  max_output : Int32,
  max_bindings : Int32,
  max_value_length : Int32,
  max_items : Int32,
  collect_spans : Bool,
) -> Result[RenderedUri, ExpandError] {
  if max_output < 0 {
    return Err(OutputTooLong)
  }
  if max_value_length < 0 {
    return Err(InvalidLimit("max_value_length"))
  }
  if max_items < 0 {
    return Err(InvalidLimit("max_items"))
  }
  let values = match
    prepare_bindings(bindings, max_bindings, max_value_length, max_items) {
    Ok(value) => value
    Err(error) => return Err(error)
  }
  let out = StringBuilder()
  let spans : Array[OutputSpan] = []
  let mut used = 0
  let mut source_cursor = 0
  for segment in self.segments {
    let (source_start, source_end, expression) = match segment {
      Literal(raw) => (source_cursor, source_cursor + raw.length(), false)
      Expression(_, _, start, end) => (start, end, true)
    }
    let text = match segment {
      Literal(raw) =>
        match encode_literal_limited(raw, max_output - used) {
          Ok(value) => value
          Err(error) => return Err(error)
        }
      Expression(op, specs, _, _) =>
        match expand_expression(op, specs, values, max_output - used) {
          Ok(text) => text
          Err(error) => return Err(error)
        }
    }
    if text.length() > max_output - used {
      return Err(OutputTooLong)
    }
    out.write_string(text)
    if collect_spans {
      spans.push({
        source_start,
        source_end,
        output_start: used,
        output_end: used + text.length(),
        is_expression: expression,
      })
    }
    used += text.length()
    source_cursor = source_end
  }
  Ok({ text: out.to_string(), spans, })
}

///|
fn prepare_bindings(
  bindings : Array[(String, Value)],
  max_bindings : Int32,
  max_value_length : Int32,
  max_items : Int32,
) -> Result[Map[String, Value], ExpandError] {
  if max_bindings < 0 || bindings.length() > max_bindings {
    return Err(TooManyBindings)
  }
  let values : Map[String, Value] = Map([])
  for binding in bindings {
    let (name, value) = binding
    if values.contains(name) {
      return Err(DuplicateVariable(name))
    }
    match check_value(value, name, max_value_length, max_items) {
      Ok(_) => ()
      Err(error) => return Err(error)
    }
    values[name] = value
  }
  Ok(values)
}

///|
fn check_text(
  text : String,
  name : String,
  max_value_length : Int32,
) -> Result[Unit, ExpandError] {
  if max_value_length < 0 || text.length() > max_value_length {
    Err(ValueTooLong(name))
  } else if first_invalid_utf16(text) is Some(_) {
    Err(InvalidValue(name))
  } else {
    Ok(())
  }
}

///|
fn check_value(
  value : Value,
  name : String,
  max_value_length : Int32,
  max_items : Int32,
) -> Result[Unit, ExpandError] {
  match value {
    Scalar(text) => check_text(text, name, max_value_length)
    List(items) => {
      if max_items < 0 || items.length() > max_items {
        return Err(TooManyItems(name))
      }
      for item in items {
        match check_text(item, name, max_value_length) {
          Ok(_) => ()
          Err(error) => return Err(error)
        }
      }
      Ok(())
    }
    Assoc(entries) => {
      if max_items < 0 || entries.length() > max_items {
        return Err(TooManyItems(name))
      }
      for entry in entries {
        let (key, item) = entry
        match check_text(key, name, max_value_length) {
          Ok(_) => ()
          Err(error) => return Err(error)
        }
        match check_text(item, name, max_value_length) {
          Ok(_) => ()
          Err(error) => return Err(error)
        }
      }
      Ok(())
    }
    SparseList(items) => {
      if max_items < 0 || items.length() > max_items {
        return Err(TooManyItems(name))
      }
      for item in items {
        match item {
          Some(text) =>
            match check_text(text, name, max_value_length) {
              Ok(_) => ()
              Err(error) => return Err(error)
            }
          None => ()
        }
      }
      Ok(())
    }
    SparseAssoc(entries) => {
      if max_items < 0 || entries.length() > max_items {
        return Err(TooManyItems(name))
      }
      for entry in entries {
        let (key, item) = entry
        match check_text(key, name, max_value_length) {
          Ok(_) => ()
          Err(error) => return Err(error)
        }
        match item {
          Some(text) =>
            match check_text(text, name, max_value_length) {
              Ok(_) => ()
              Err(error) => return Err(error)
            }
          None => ()
        }
      }
      Ok(())
    }
  }
}

///|
fn operator_prefix(op : Operator) -> String {
  match op {
    Simple | Reserved => ""
    Fragment => "#"
    Label => "."
    Path => "/"
    PathParameter => ";"
    Query => "?"
    QueryContinuation => "&"
  }
}

///|
fn operator_separator(op : Operator) -> String {
  match op {
    Simple | Reserved | Fragment => ","
    Label => "."
    Path => "/"
    PathParameter => ";"
    Query | QueryContinuation => "&"
  }
}

///|
fn operator_named(op : Operator) -> Bool {
  match op {
    PathParameter | Query | QueryContinuation => true
    _ => false
  }
}

///|
fn operator_form(op : Operator) -> Bool {
  op is Query || op is QueryContinuation
}

///|
fn operator_allows_reserved(op : Operator) -> Bool {
  op is Reserved || op is Fragment
}

///|
fn join_text(parts : Array[String], separator : String) -> String {
  let out = StringBuilder()
  for i, part in parts {
    if i > 0 {
      out.write_string(separator)
    }
    out.write_string(part)
  }
  out.to_string()
}

///|
priv struct PieceBuffer {
  pieces : Array[String]
  separator : String
  limit : Int32
  mut used : Int32
}

///|
fn PieceBuffer::new(separator : String, limit : Int32) -> PieceBuffer {
  { pieces: [], separator, limit, used: 0, }
}

///|
fn PieceBuffer::push(
  self : PieceBuffer,
  piece : String,
) -> Result[Unit, ExpandError] {
  let extra = if self.pieces.is_empty() { 0 } else { self.separator.length() }
  if self.used > self.limit - extra ||
    piece.length() > self.limit - extra - self.used {
    return Err(OutputTooLong)
  }
  self.pieces.push(piece)
  self.used += extra + piece.length()
  Ok(())
}

///|
fn PieceBuffer::join(self : PieceBuffer) -> String {
  join_text(self.pieces, self.separator)
}

///|
fn expand_expression(
  op : Operator,
  specs : Array[VarSpec],
  values : Map[String, Value],
  limit : Int32,
) -> Result[String, ExpandError] {
  let prefix = operator_prefix(op)
  let pieces = PieceBuffer::new(operator_separator(op), limit - prefix.length())
  for spec in specs {
    match values.get(spec.name) {
      None => ()
      Some(value) =>
        match expand_variable(op, spec, value, limit) {
          Ok(items) =>
            for item in items {
              match pieces.push(item) {
                Ok(_) => ()
                Err(error) => return Err(error)
              }
            }
          Err(error) => return Err(error)
        }
    }
  }
  if pieces.pieces.is_empty() {
    Ok("")
  } else {
    Ok(prefix + pieces.join())
  }
}

///|
fn named_piece(op : Operator, name : String, value : String) -> String {
  if !operator_named(op) {
    return value
  }
  if value == "" && !operator_form(op) {
    name
  } else {
    name + "=" + value
  }
}

///|
fn expand_variable(
  op : Operator,
  spec : VarSpec,
  value : Value,
  limit : Int32,
) -> Result[Array[String], ExpandError] {
  let reserved = operator_allows_reserved(op)
  match value {
    Scalar(raw) => {
      let selected = match spec.prefix {
        None => raw
        Some(length) => scalar_prefix(raw, length)
      }
      let encoded = match encode_value_limited(selected, reserved, limit) {
        Ok(value) => value
        Err(error) => return Err(error)
      }
      let piece = named_piece(op, spec.name, encoded)
      if piece.length() > limit {
        Err(OutputTooLong)
      } else {
        Ok([piece])
      }
    }
    List(items) => {
      if spec.prefix is Some(_) {
        return Err(InvalidValue(spec.name))
      }
      if items.is_empty() {
        return Ok([])
      }
      if spec.explode {
        let pieces = PieceBuffer::new(operator_separator(op), limit)
        for item in items {
          let encoded = match encode_value_limited(item, reserved, limit) {
            Ok(value) => value
            Err(error) => return Err(error)
          }
          match pieces.push(named_piece(op, spec.name, encoded)) {
            Ok(_) => ()
            Err(error) => return Err(error)
          }
        }
        Ok(pieces.pieces)
      } else {
        let encoded = PieceBuffer::new(",", limit)
        for item in items {
          let value = match encode_value_limited(item, reserved, limit) {
            Ok(value) => value
            Err(error) => return Err(error)
          }
          match encoded.push(value) {
            Ok(_) => ()
            Err(error) => return Err(error)
          }
        }
        let piece = named_piece(op, spec.name, encoded.join())
        if piece.length() > limit {
          Err(OutputTooLong)
        } else {
          Ok([piece])
        }
      }
    }
    Assoc(entries) => {
      if spec.prefix is Some(_) {
        return Err(InvalidValue(spec.name))
      }
      if entries.is_empty() {
        return Ok([])
      }
      if spec.explode {
        let pieces = PieceBuffer::new(operator_separator(op), limit)
        for entry in entries {
          let (name, item) = entry
          let key = match encode_value_limited(name, reserved, limit) {
            Ok(value) => value
            Err(error) => return Err(error)
          }
          let encoded = match encode_value_limited(item, reserved, limit) {
            Ok(value) => value
            Err(error) => return Err(error)
          }
          let piece = if encoded == "" && !operator_form(op) {
            key
          } else {
            key + "=" + encoded
          }
          match pieces.push(piece) {
            Ok(_) => ()
            Err(error) => return Err(error)
          }
        }
        Ok(pieces.pieces)
      } else {
        let encoded = PieceBuffer::new(",", limit)
        for entry in entries {
          let (name, item) = entry
          let key = match encode_value_limited(name, reserved, limit) {
            Ok(value) => value
            Err(error) => return Err(error)
          }
          match encoded.push(key) {
            Ok(_) => ()
            Err(error) => return Err(error)
          }
          let value = match encode_value_limited(item, reserved, limit) {
            Ok(value) => value
            Err(error) => return Err(error)
          }
          match encoded.push(value) {
            Ok(_) => ()
            Err(error) => return Err(error)
          }
        }
        let piece = named_piece(op, spec.name, encoded.join())
        if piece.length() > limit {
          Err(OutputTooLong)
        } else {
          Ok([piece])
        }
      }
    }
    SparseList(items) => {
      let defined : Array[String] = []
      for item in items {
        match item {
          Some(text) => defined.push(text)
          None => ()
        }
      }
      expand_variable(op, spec, List(defined), limit)
    }
    SparseAssoc(entries) => {
      let defined : Array[(String, String)] = []
      for entry in entries {
        let (name, item) = entry
        match item {
          Some(text) => defined.push((name, text))
          None => ()
        }
      }
      expand_variable(op, spec, Assoc(defined), limit)
    }
  }
}