///|
pub struct Arguments {
  flags : Map[String, String]
  values : Array[String]
  json_form : Bool
} derive(Debug, Eq, ToJson)

///|
/// Split a Docker argument list without evaluating variables or shell commands.
fn split_quoted(source : String) -> Array[String] raise ParseError {
  let out = []
  let mut text = StringBuilder()
  let mut quote : Char? = None
  let mut escape = false
  let mut started = false
  for c in source.iter() {
    if escape {
      text.write_char(c)
      escape = false
      continue
    }
    if c == '\\' && quote != Some('\'') {
      escape = true
      started = true
      continue
    }
    if quote is Some(q) {
      if c == q {
        quote = None
      } else {
        text.write_char(c)
      }
      continue
    }
    if c == '"' || c == '\'' {
      quote = Some(c)
      started = true
    } else if c == ' ' || c == '\t' {
      if started {
        out.push(text.to_string())
        text = StringBuilder()
        started = false
      }
    } else {
      text.write_char(c)
      started = true
    }
  }
  if quote != None || escape {
    raise Invalid("unterminated quoted argument")
  }
  if started {
    out.push(text.to_string())
  }
  out
}

///|
/// Leading --key=value flags, then JSON strings or quoted shell-form arguments.
pub fn Instruction::split_arguments(
  self : Instruction,
) -> Arguments raise ParseError {
  let flags : Map[String, String] = Map([])
  let mut rest = self.arguments.trim_start().to_owned()
  while rest.has_prefix("--") {
    let cs = rest.to_array()
    let mut end = 0
    let mut quote : Char? = None
    while end < cs.length() {
      let c = cs[end]
      if quote is Some(q) {
        if c == q {
          quote = None
        }
      } else if c == '\'' || c == '"' {
        quote = Some(c)
      } else if c == ' ' || c == '\t' {
        break
      }
      end += 1
    }
    let raw = String::from_array(cs[:end])
    let decoded = split_quoted(raw)
    if decoded.length() != 1 {
      raise Invalid("invalid instruction flag")
    }
    let flag = decoded[0][2:].to_owned()
    let parts = flag.split("=").collect()
    let key = parts[0].to_owned()
    if key.is_empty() {
      raise Invalid("empty instruction flag")
    }
    // Repeated flags such as COPY --exclude and RUN --mount are retained in the source;
    // this convenience map exposes their last value.
    flags[key] = if parts.length() == 1 {
      "true"
    } else {
      parts[1:].map(x => x.to_owned()).join("=")
    }
    rest = String::from_array(cs[end:]).trim_start().to_owned()
  }
  let json_form = rest.has_prefix("[")
  let values = if json_form {
    let json = @json.parse(rest) catch {
      _ => raise Invalid("malformed JSON instruction")
    }
    match json {
      Array(items) =>
        items.map(item => {
          match item {
            String(s) => s
            _ => raise Invalid("JSON instruction requires strings")
          }
        })
      _ => raise Invalid("JSON instruction requires array")
    }
  } else {
    split_quoted(rest)
  }
  if values.is_empty() {
    raise Invalid("empty instruction arguments")
  }
  { flags, values, json_form, }
}

///|
// ENV/LABEL words preserve shell punctuation as literal data.
fn raw_arguments(source : String) -> Array[String] raise ParseError {
  let cs = source.to_array()
  let result : Array[String] = []
  let mut start = 0
  let mut quote : Char? = None
  let mut nesting = 0
  let mut i = 0
  while i < cs.length() {
    let c = cs[i]
    if c == '\\' && quote != Some('\'') {
      i += 2
      continue
    }
    if quote is Some(q) {
      if c == q {
        quote = None
      }
    } else if c == '\'' || c == '"' {
      quote = Some(c)
    } else if c == '$' && i + 1 < cs.length() && cs[i + 1] == '{' {
      nesting += 1
      i += 1
    } else if c == '}' && nesting > 0 {
      nesting -= 1
    } else if (c == ' ' || c == '\t') && nesting == 0 {
      if start < i {
        result.push(String::from_array(cs[start:i]))
      }
      start = i + 1
    }
    i += 1
  }
  if quote != None || nesting != 0 || i > cs.length() {
    raise Invalid("invalid Docker argument quoting")
  }
  if start < cs.length() {
    result.push(String::from_array(cs[start:]))
  }
  result
}