///|
priv struct LogicalLine {
  text : String
  line : Int
}

///|
fn strip_quotes(value : String) -> String {
  if value.length() >= 2 &&
    (
      (value.has_prefix("'") && value.has_suffix("'")) ||
      (value.has_prefix("\"") && value.has_suffix("\""))
    ) {
    value[1:value.length() - 1].to_owned()
  } else {
    value
  }
}

///|
fn heredoc_delimiter(line : String) -> String? {
  match line.find("<<") {
    None => None
    Some(index) => {
      let mut rest = line[index + 2:].to_owned()
      if rest.has_prefix("-") {
        rest = rest[1:].to_owned()
      }
      let (token, _) = first_token(rest)
      let delim = strip_quotes(token)
      if delim.is_empty() || delim.has_prefix("<") {
        None
      } else {
        Some(delim)
      }
    }
  }
}

///|
fn join_continuations(source : String) -> Array[LogicalLine] {
  let physical = split_physical_lines(source)
  let result : Array[LogicalLine] = []
  let mut i = 0
  while i < physical.length() {
    let start_line = i + 1
    let buf = StringBuilder()
    let mut continuing = true
    while continuing && i < physical.length() {
      let raw = physical[i]
      let trimmed = raw.trim_end().to_owned()
      let is_comment = trimmed.trim().has_prefix("#")
      if !is_comment && trimmed.has_suffix("\\") {
        buf.write_string(trimmed[:trimmed.length() - 1].to_owned())
        buf.write_char(' ')
        i += 1
      } else {
        buf.write_string(raw)
        i += 1
        match heredoc_delimiter(trimmed) {
          None => ()
          Some(delim) =>
            while i < physical.length() {
              buf.write_char('\n')
              buf.write_string(physical[i])
              let closed = physical[i].trim().to_owned() == delim
              i += 1
              if closed {
                break
              }
            }
        }
        continuing = false
      }
    }
    result.push({ text: buf.to_string(), line: start_line, })
  }
  result
}

///|
pub fn parse_dockerfile(source : String) -> Result[Dockerfile, ParseError] {
  let instructions : Array[Instruction] = []
  let mut current_stage : String? = None
  let mut pending_ignores : Array[String] = []
  for logical in join_continuations(source) {
    let text = logical.text.trim().to_owned()
    if text.is_empty() {
      continue
    }
    if text.has_prefix("#") {
      match parse_ignore_comment(text) {
        None => ()
        Some(codes) =>
          for code in codes {
            pending_ignores.push(code)
          }
      }
      continue
    }
    let (name, rest) = first_token(text)
    if name.is_empty() {
      return Err({ line: logical.line, message: "missing instruction", })
    }
    let kind = kind_of(name)
    let arguments = rest.trim().to_owned()
    if kind is From {
      current_stage = parse_from_stage(arguments)
    }
    let copy_from = if kind is Copy || kind is Add {
      flag_value(arguments, "from")
    } else {
      None
    }
    let ignored_codes = pending_ignores
    pending_ignores = []
    let form = form_of(rest)
    instructions.push({
      kind,
      name,
      arguments,
      line: logical.line,
      form,
      stage: current_stage,
      copy_from,
      ignored_codes,
      node: parse_node(kind, arguments, form),
    })
  }
  Ok({ instructions, })
}

///|
pub fn lint_source(source : String) -> Result[LintResult, ParseError] {
  lint_source_with(source, default_options())
}

///|
pub fn lint_source_with(
  source : String,
  options : LintOptions,
) -> Result[LintResult, ParseError] {
  match parse_dockerfile(source) {
    Err(error) => Err(error)
    Ok(doc) => Ok(lint_dockerfile_with(doc, options))
  }
}