// URI -- the `format: uri` of the Lexicon, used for external embed links,
// avatar URLs and the `uri` on a label.
//
// Ported from @atproto/syntax packages/syntax/src/uri.ts, which is all of four
// lines: `/^\w+:(?:\/\/)?[^\s/][^\s]*$/`.
//
// Deliberately loose, and worth saying why rather than tightening it. This
// format covers every scheme anyone might link to, and the values are shown to
// a person or handed to an HTTP client -- neither of which this library does.
// A validator strict enough to be useful here would have to be a full RFC 3986
// parser, and would then reject real links that browsers accept, which is a
// worse failure than passing one through.
//
// If you need "is this a link I should fetch", check the scheme yourself. This
// answers only "is this shaped like a URI at all".

///|
/// A string shaped like a URI.
pub struct Uri(String) derive(Eq, Debug)

///|
pub impl Show for Uri with fn output(self, logger) {
  logger.write_string(self.0)
}

///|
pub fn Uri::to_string(self : Self) -> String {
  self.0
}

///|
pub fn Uri::unchecked(uri : String) -> Uri {
  Uri(uri)
}

///|
pub fn Uri::is_valid(uri : String) -> Bool {
  try {
    Uri::parse(uri) |> ignore
    true
  } catch {
    _ => false
  }
}

///|
/// The scheme, lower-cased -- `https` in `https://example.com`. This is the
/// part worth branching on, and the reason to have a type here at all.
pub fn Uri::scheme(self : Self) -> String {
  match self.0.split_once(":") {
    Some((scheme, _)) => scheme.to_owned().to_lower()
    // Unreachable: `parse` requires a colon.
    None => ""
  }
}

///|
pub fn Uri::parse(uri : String) -> Uri raise SyntaxError {
  fn bad(reason : String) -> SyntaxError {
    SyntaxError(kind=Uri, input=uri, reason~)
  }

  guard uri.split_once(":") is Some((scheme, rest)) else {
    raise bad("URI must have a scheme")
  }
  // `\w+` -- and note it must be non-empty, so `://x` has no scheme.
  guard scheme.length() > 0 && all_chars(scheme.to_owned(), is_word_char) else {
    raise bad("URI scheme must be letters, digits or underscore")
  }
  // The authority marker is optional: `mailto:a@b` and `did:plc:x` are URIs.
  let after = if rest.to_owned().has_prefix("//") {
    rest.to_owned()[2:].to_owned()
  } else {
    rest.to_owned()
  }
  // `[^\s/][^\s]*` -- non-empty, and not starting with a slash, which is what
  // rejects `https:///path` and a bare `at://`.
  guard after.length() > 0 else { raise bad("URI must have a path or host") }
  guard !after.has_prefix("/") else {
    raise bad("URI must have a path or host")
  }
  guard all_chars(after, c => !is_whitespace(c)) else {
    raise bad("URI can not contain whitespace")
  }
  Uri(uri)
}

///|
/// `\w` -- ASCII letters, digits and underscore.
fn is_word_char(c : Char) -> Bool {
  is_ascii_alnum(c) || c == '_'
}

///|
fn is_whitespace(c : Char) -> Bool {
  c == ' ' || c == '\t' || c == '\n' || c == '\r'
}