///| Path, Query, Fragment
typealias (String, String?, String?) as LocationTuple

///|
pub suberror LocationParseError {
  ShouldStartWithSlash
  MoreThanOneQuery
  MoreThanOneFragment
  FragmentBeforeQuery
  EmptyLocation
  InvalidCharacter(Int)
} derive(Eq, Show)

// TODO: Add char check
// Unreserved Characters
// https://datatracker.ietf.org/doc/html/rfc3986#section-2.3
// Unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
// Reserved Characters
// https://datatracker.ietf.org/doc/html/rfc3986#section-2.2
// Reserved = gen-delims / sub-delims
// gen-delims = ":" / "/" / "?" / "#" / "[" / "]" / "@"
// sub-delims = "!" / "$" / "&" / "'" / "(" / ")" / "*" / "+" / "," /
//              ";" / "="

///|
fn parse_location(location : String) -> LocationTuple raise LocationParseError {
  if location.length() == 0 {
    raise LocationParseError::EmptyLocation
  }
  if location[0] != '/' {
    raise LocationParseError::ShouldStartWithSlash
  }
  // "/"
  if location.length() == 1 {
    return ("/", None, None)
  }
  let mut query_start = -1
  let mut fragment_start = -1
  for index, char in location.iter2() {
    // TODO: Add more checks for valid characters
    if char == ' ' {
      raise LocationParseError::InvalidCharacter(index)
    }
    if char == '?' {
      if query_start != -1 {
        raise LocationParseError::MoreThanOneQuery
      }
      query_start = index
    } else if char == '#' {
      if fragment_start != -1 {
        raise LocationParseError::MoreThanOneFragment
      }
      fragment_start = index
    }
  }
  if query_start != -1 && fragment_start != -1 && fragment_start < query_start {
    raise LocationParseError::FragmentBeforeQuery
  }
  let path = if query_start != -1 {
    location.view(end_offset=query_start).to_string()
  } else if fragment_start != -1 {
    location.view(end_offset=fragment_start).to_string()
  } else {
    location
  }
  let query = if query_start == -1 {
    None
  } else if fragment_start != -1 {
    Some(
      location
      .view(start_offset=query_start + 1, end_offset=fragment_start)
      .to_string(),
    )
  } else {
    Some(location.view(start_offset=query_start + 1).to_string())
  }
  let fragment = if fragment_start == -1 {
    None
  } else {
    Some(location.view(start_offset=fragment_start + 1).to_string())
  }
  (path, query, fragment)
}

///|
test "parse_location/basic_parsing" {
  inspect(
    parse_location("/path?query#fragment"),
    content="(\"/path\", Some(\"query\"), Some(\"fragment\"))",
  )
  inspect(
    parse_location("/path#fragment"),
    content="(\"/path\", None, Some(\"fragment\"))",
  )
  inspect(
    parse_location("/path?query"),
    content="(\"/path\", Some(\"query\"), None)",
  )
  inspect(parse_location("/path"), content="(\"/path\", None, None)")
}

///|
test "parse_location/edge_cases" {
  inspect(parse_location("/?query"), content="(\"/\", Some(\"query\"), None)")
  inspect(
    parse_location("/#fragment"),
    content="(\"/\", None, Some(\"fragment\"))",
  )
  inspect(parse_location("/"), content="(\"/\", None, None)")
}

///|
test "parse_location/invalid_cases" {
  assert_eq(
    try? parse_location("invalid_no_slash"),
    Err(LocationParseError::ShouldStartWithSlash),
  )
  // Invalid character in path
  assert_eq(
    try? parse_location("/path with space"),
    Err(LocationParseError::InvalidCharacter(5)),
  )
  // Cannot have multiple query parameters
  assert_eq(
    try? parse_location("/path?query1?query2"),
    Err(LocationParseError::MoreThanOneQuery),
  )
  // Cannot have multiple fragments
  assert_eq(
    try? parse_location("/path#fragment1#fragment2"),
    Err(LocationParseError::MoreThanOneFragment),
  )
  // Fragment must come after query
  assert_eq(
    try? parse_location("/path#fragment?query"),
    Err(LocationParseError::FragmentBeforeQuery),
  )
}

///|
test "parse_location/empty" {
  assert_eq(try? parse_location(""), Err(LocationParseError::EmptyLocation))
}

///|
test "parse_location/empty_path" {
  inspect(parse_location("/?#"), content="(\"/\", Some(\"\"), Some(\"\"))")
}