///|
/// Bash-style variable substitution, as Compose specifies it.
///
/// Supported forms: `$VAR`, `${VAR}`, `${VAR:-default}` (default when unset or
/// empty), `${VAR-default}` (default when unset), `${VAR:?err}` / `${VAR?err}`
/// (fail when unset), `${VAR:+alt}` / `${VAR+alt}` (alternate when set), and
/// `$$` for a literal `$`.

///|
/// Substitute every variable reference in one string.
pub fn interp_str(
  value : String,
  env : Map[String, String],
) -> String raise Error {
  if !value.contains_char('$') {
    return value
  }
  let out = StringBuilder(size_hint=value.length())
  let chars = value.to_array()
  let mut i = 0
  while i < chars.length() {
    let c = chars[i]
    if c != '$' {
      out.write_char(c)
      i += 1
      continue
    }
    if i + 1 >= chars.length() {
      out.write_char('$')
      i += 1
      continue
    }
    let next = chars[i + 1]
    if next == '$' {
      out.write_char('$')
      i += 2
      continue
    }
    if next == '{' {
      let closing = find_closing_brace(chars, i + 1)
      if closing < 0 {
        raise fail(
          "no closing brace found for variable interpolation in \"\{value}\"",
        )
      }
      let content = String::from_array(chars[i + 2:closing])
      out.write_string(resolve_brace(content, env))
      i = closing + 1
      continue
    }
    if is_var_start(next) {
      let mut j = i + 1
      let name = StringBuilder()
      while j < chars.length() && is_var_char(chars[j]) {
        name.write_char(chars[j])
        j += 1
      }
      out.write_string(env.get(name.to_string()).unwrap_or(""))
      i = j
      continue
    }
    out.write_char('$')
    i += 1
  }
  out.to_string()
}

///|
/// The index of the `}` matching the `{` at `open`, or -1.
fn find_closing_brace(chars : Array[Char], open : Int32) -> Int32 {
  let mut level = 0
  let mut i = open
  while i < chars.length() {
    if chars[i] == '{' {
      level += 1
    } else if chars[i] == '}' {
      level -= 1
      if level == 0 {
        return i
      }
    }
    i += 1
  }
  -1
}

///|
/// Resolve the inside of a `${...}` reference.
fn resolve_brace(
  content : String,
  env : Map[String, String],
) -> String raise Error {
  let chars = content.to_array()
  if chars.length() == 0 || !is_var_start(chars[0]) {
    raise fail(
      "invalid interpolation format: $\{" + content + "} (escape a literal $ as $$)",
    )
  }
  let mut i = 0
  let name = StringBuilder()
  while i < chars.length() && is_var_char(chars[i]) {
    name.write_char(chars[i])
    i += 1
  }
  let key = name.to_string()
  if i >= chars.length() {
    return env.get(key).unwrap_or("")
  }
  let rest = content[i:].to_owned()
  let (op, operand) = if rest.has_prefix(":-") {
    (":-", rest[2:].to_owned())
  } else if rest.has_prefix(":?") {
    (":?", rest[2:].to_owned())
  } else if rest.has_prefix(":+") {
    (":+", rest[2:].to_owned())
  } else if rest.has_prefix("-") {
    ("-", rest[1:].to_owned())
  } else if rest.has_prefix("?") {
    ("?", rest[1:].to_owned())
  } else if rest.has_prefix("+") {
    ("+", rest[1:].to_owned())
  } else {
    raise fail("invalid variable interpolation syntax: $\{" + content + "}")
  }
  apply_operator(key, op, operand, env)
}

///|
/// The operator semantics, matching the Compose spec.
fn apply_operator(
  name : String,
  op : String,
  operand : String,
  env : Map[String, String],
) -> String raise Error {
  let value = env.get(name)
  match op {
    ":-" =>
      match value {
        Some(v) if v.length() > 0 => v
        _ => interp_str(operand, env)
      }
    "-" =>
      match value {
        Some(v) => v
        None => interp_str(operand, env)
      }
    ":?" =>
      match value {
        Some(v) if v.length() > 0 => v
        _ => {
          let detail = if operand.length() > 0 {
            interp_str(operand, env)
          } else {
            ""
          }
          raise fail("required variable \{name} is missing a value: \{detail}")
        }
      }
    "?" =>
      match value {
        Some(v) => v
        None => {
          let detail = if operand.length() > 0 {
            interp_str(operand, env)
          } else {
            ""
          }
          raise fail("required variable \{name} is missing a value: \{detail}")
        }
      }
    ":+" =>
      match value {
        Some(v) if v.length() > 0 => interp_str(operand, env)
        _ => ""
      }
    "+" =>
      match value {
        Some(_) => interp_str(operand, env)
        None => ""
      }
    _ => raise fail("unknown operator \{op} in variable interpolation")
  }
}

///|
fn is_var_start(c : Char) -> Bool {
  (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '_'
}

///|
fn is_var_char(c : Char) -> Bool {
  is_var_start(c) || (c >= '0' && c <= '9')
}

///|
/// Interpolate every string in a value tree.
///
/// Within a service mapping, the service's own `environment` entries are added
/// to the substitution scope first, so `command: "$${PORT}"` can see a `PORT`
/// the service sets. Variables already in `env` win, matching the reference
/// implementation.
pub fn rec_subs(
  v : @yaml.Yaml,
  env : Map[String, String],
) -> @yaml.Yaml raise Error {
  match v {
    @yaml.Yaml::String(s) => y_str_v(interp_str(s, env))
    @yaml.Yaml::Array(a) => {
      let out : Array[@yaml.Yaml] = []
      for item in a {
        out.push(rec_subs(item, env))
      }
      y_arr_v(out)
    }
    @yaml.Yaml::Map(m) => {
      let scope = service_scope(m, env)
      let out : Map[String, @yaml.Yaml] = Map([])
      for entry in m {
        let (k, value) = entry
        out[interp_str(k, scope)] = rec_subs(value, scope)
      }
      y_map_v(out)
    }
    _ => v
  }
}

///|
/// Build the substitution scope for a mapping: the ambient environment plus any
/// `environment` entries the mapping defines that the ambient one lacks.
fn service_scope(
  m : Map[String, @yaml.Yaml],
  env : Map[String, String],
) -> Map[String, String] raise Error {
  let scope : Map[String, String] = Map([])
  for entry in env {
    let (k, value) = entry
    scope[k] = value
  }
  match m.get("environment") {
    Some(env_value) =>
      match y_map(env_value) {
        Some(em) =>
          for entry in em {
            let (k, value) = entry
            if !scope.contains(k) {
              match y_str(value) {
                Some(s) => scope[k] = interp_str(s, scope)
                None => ()
              }
            }
          }
        None => ()
      }
    None => ()
  }
  scope
}