///|
fn includegraphics_spec() -> FunctionSpec {
  FunctionSpec::make(
    ["\\includegraphics"],
    1,
    num_optional_args=1,
    arg_types=[RawArg, UrlArg],
    handler=includegraphics_handler,
  )
}

///|
fn graphics_number(text : String) -> String? {
  let mut index = 0
  let mut sign = ""
  if index < text.length() && (text[index] == '+' || text[index] == '-') {
    sign = text.unsafe_substring(start=0, end=1)
    index = 1
    while index < text.length() && text[index] == ' ' {
      index = index + 1
    }
  }
  let start = index
  while index < text.length() && text[index] >= '0' && text[index] <= '9' {
    index = index + 1
  }
  let before = index - start
  let mut after = 0
  if index < text.length() && text[index] == '.' {
    index = index + 1
    let fraction_start = index
    while index < text.length() && text[index] >= '0' && text[index] <= '9' {
      index = index + 1
    }
    after = index - fraction_start
  }
  if index != text.length() || (before == 0 && after == 0) {
    None
  } else {
    Some(sign + text.unsafe_substring(start~, end=text.length()))
  }
}

///|
fn graphics_size(text : String) -> Measurement raise ParseFailure {
  let text = trim_ascii_spaces(text)
  if graphics_number(text) is Some(number_text) {
    return { number: parse_decimal(number_text), unit: "bp" }
  }
  match parse_size_measurement(text) {
    Some(size) if valid_size_unit(size.unit) => size
    Some(size) =>
      raise InvalidArgument(
        message="Invalid unit: '" + size.unit + "' in \\includegraphics.",
        loc=None,
      )
    None =>
      raise InvalidArgument(
        message="Invalid size: '" + text + "' in \\includegraphics",
        loc=None,
      )
  }
}

///|
fn graphics_option_items(value : String) -> Array[String] {
  let items : Array[String] = []
  let mut start = 0
  for index = 0; index <= value.length(); index = index + 1 {
    if index == value.length() || value[index] == ',' {
      items.push(value.unsafe_substring(start~, end=index))
      start = index + 1
    }
  }
  items
}

///|
fn graphics_option_pair(item : String) -> (String, String)? {
  let mut equals = -1
  let mut count = 0
  for index = 0; index < item.length(); index = index + 1 {
    if item[index] == '=' {
      equals = index
      count = count + 1
    }
  }
  if count != 1 {
    None
  } else {
    Some(
      (
        trim_ascii_spaces(item.unsafe_substring(start=0, end=equals)),
        trim_ascii_spaces(
          item.unsafe_substring(start=equals + 1, end=item.length()),
        ),
      ),
    )
  }
}

///|
fn graphics_default_alt(src : String) -> String {
  let mut name_start = 0
  let mut extension = -1
  for index = 0; index < src.length(); index = index + 1 {
    if src[index] == '/' || src[index] == '\\' {
      name_start = index + 1
      extension = -1
    } else if src[index] == '.' {
      extension = index
    }
  }
  if extension < name_start {
    ""
  } else {
    src.unsafe_substring(start=name_start, end=extension)
  }
}

///|
fn includegraphics_handler(
  context : FunctionContext,
  args : Array[ParseNode],
  opt_args : Array[ParseNode?],
) -> ParseNode raise ParseFailure {
  let mut width = { number: 0.0, unit: "em" }
  let mut height = { number: 0.9, unit: "em" }
  let mut totalheight = { number: 0.0, unit: "em" }
  let mut alt = ""
  match opt_args.get(0) {
    Some(Some(Raw(string=value, ..))) =>
      for item in graphics_option_items(value) {
        guard graphics_option_pair(item) is Some((key, value)) else { continue }
        match key {
          "alt" => alt = value
          "width" => width = graphics_size(value)
          "height" => height = graphics_size(value)
          "totalheight" => totalheight = graphics_size(value)
          _ =>
            raise InvalidArgument(
              message="Invalid key: '" + key + "' in \\includegraphics.",
              loc=None,
            )
        }
      }
    Some(Some(_)) =>
      raise InternalInvariant(message="Expected raw graphics options")
    Some(None) | None => ()
  }
  guard require_function_arg(args, 0, context.func_name) is Url(url=src, ..) else {
    raise InternalInvariant(
      message="Expected URL argument for \\includegraphics",
    )
  }
  if alt == "" {
    alt = graphics_default_alt(src)
  }
  if (context.is_trusted)(
      UrlTrust(command="\\includegraphics", url=src, protocol=None),
    ) {
    IncludeGraphics(
      mode=context.mode,
      alt~,
      width~,
      height~,
      totalheight~,
      src~,
    )
  } else {
    TextOrd(mode=Text, loc=None, text=alt)
  }
}