///|
fn parse_int_token(
  token : String,
  line : Int,
  label : String,
) -> Result[Int, PackError] {
  try @string.parse_int(token) catch {
    _ => Err(ParseError(line~, message="invalid \{label}: \{token}"))
  } noraise {
    value => Ok(value)
  }
}

///|
fn parse_bool_token(token : String, line : Int) -> Result[Bool, PackError] {
  match token {
    "true" | "yes" | "on" | "1" => Ok(true)
    "false" | "no" | "off" | "0" => Ok(false)
    _ => Err(ParseError(line~, message="invalid boolean: \{token}"))
  }
}

///|
fn parse_algorithm_token(
  token : String,
  line : Int,
) -> Result[Algorithm, PackError] {
  match token {
    "shelf" | "shelf-first-fit" => Ok(ShelfFirstFit)
    "guillotine" | "guillotine-best-area" => Ok(GuillotineBestArea)
    "skyline" | "skyline-bottom-left" => Ok(SkylineBottomLeft)
    _ => Err(ParseError(line~, message="unknown algorithm: \{token}"))
  }
}

///|
fn parse_sort_token(token : String, line : Int) -> Result[SortOrder, PackError] {
  match token {
    "input" => Ok(InputOrder)
    "area" | "area-desc" => Ok(AreaDesc)
    "height" | "height-desc" => Ok(HeightDesc)
    "width" | "width-desc" => Ok(WidthDesc)
    "perimeter" | "perimeter-desc" => Ok(PerimeterDesc)
    _ => Err(ParseError(line~, message="unknown sort order: \{token}"))
  }
}

///|
fn tokens_for_line(raw : StringView) -> Array[String] {
  let body = match raw.split_once("#") {
    Some((left, _)) => left.trim()
    None => raw.trim()
  }
  let tokens : Array[String] = []
  for part in body.split(" ") {
    let p = part.trim()
    if !p.is_empty() {
      tokens.push(p.to_owned())
    }
  }
  tokens
}

///|
/// Parse a small line-oriented specification.
///
/// Supported directives:
/// - `bin  `
/// - `padding  `
/// - `algorithm shelf|guillotine`
/// - `sort input|area|height|width|perimeter`
/// - `rotate true|false`
/// - `rect   `
pub fn parse_spec(source : String) -> Result[Spec, PackError] {
  let rects : Array[Rect] = []
  let mut bin = Bin::{ width: 64, height: 64 }
  let mut padding = Padding::{ outer: 0, inner: 0 }
  let mut algorithm = ShelfFirstFit
  let mut sort = AreaDesc
  let mut allow_rotate = false
  let mut line = 0
  for raw in source.split("\n") {
    line = line + 1
    let tokens = tokens_for_line(raw)
    if tokens.is_empty() {
      continue
    }
    match tokens[0] {
      "bin" => {
        if tokens.length() != 3 {
          return Err(ParseError(line~, message="bin expects width and height"))
        }
        let width = match parse_int_token(tokens[1], line, "bin width") {
          Ok(v) => v
          Err(err) => return Err(err)
        }
        let height = match parse_int_token(tokens[2], line, "bin height") {
          Ok(v) => v
          Err(err) => return Err(err)
        }
        bin = Bin::{ width, height }
      }
      "padding" => {
        if tokens.length() != 3 {
          return Err(
            ParseError(line~, message="padding expects outer and inner"),
          )
        }
        let outer = match parse_int_token(tokens[1], line, "outer padding") {
          Ok(v) => v
          Err(err) => return Err(err)
        }
        let inner = match parse_int_token(tokens[2], line, "inner padding") {
          Ok(v) => v
          Err(err) => return Err(err)
        }
        padding = Padding::{ outer, inner }
      }
      "algorithm" => {
        if tokens.length() != 2 {
          return Err(ParseError(line~, message="algorithm expects one value"))
        }
        algorithm = match parse_algorithm_token(tokens[1], line) {
          Ok(v) => v
          Err(err) => return Err(err)
        }
      }
      "sort" => {
        if tokens.length() != 2 {
          return Err(ParseError(line~, message="sort expects one value"))
        }
        sort = match parse_sort_token(tokens[1], line) {
          Ok(v) => v
          Err(err) => return Err(err)
        }
      }
      "rotate" => {
        if tokens.length() != 2 {
          return Err(ParseError(line~, message="rotate expects one boolean"))
        }
        allow_rotate = match parse_bool_token(tokens[1], line) {
          Ok(v) => v
          Err(err) => return Err(err)
        }
      }
      "rect" => {
        if tokens.length() != 4 {
          return Err(ParseError(line~, message="rect expects id width height"))
        }
        let width = match parse_int_token(tokens[2], line, "rect width") {
          Ok(v) => v
          Err(err) => return Err(err)
        }
        let height = match parse_int_token(tokens[3], line, "rect height") {
          Ok(v) => v
          Err(err) => return Err(err)
        }
        rects.push(Rect::{ id: tokens[1], width, height })
      }
      other =>
        return Err(ParseError(line~, message="unknown directive: \{other}"))
    }
  }
  let options = PackOptions::{ bin, padding, sort, algorithm, allow_rotate }
  match validate_input(rects, options) {
    Ok(_) => Ok(Spec::{ options, rects })
    Err(err) => Err(err)
  }
}