///|
pub suberror DavError {
  Invalid(String)
} derive(Debug)

///|
pub(all) struct Request {
  verb : String
  target : String
  headers : Map[String, String]
  body : String
} derive(Debug, Eq, ToJson)

///|
pub(all) struct Property {
  uri : String
  name : String
  value : String
  status : Int
} derive(Debug, Eq, ToJson)

///|
pub(all) struct Resource {
  href : String
  status : Int?
  properties : Array[Property]
} derive(Debug, Eq, ToJson)

///|
fn escape(s : String) -> String {
  s
  .replace_all(old="&", new="&")
  .replace_all(old="<", new="<")
  .replace_all(old=">", new=">")
  .replace_all(old="\"", new=""")
  .replace_all(old="'", new="'")
}

///|
pub fn path(raw : String) -> String raise DavError {
  if !raw.has_prefix("/") || raw.length() > 8192 {
    raise Invalid("absolute raw path required")
  }
  let hex = "0123456789ABCDEF".to_array()
  let mut out = ""
  for b in @utf8.encode(raw) {
    let n = b.to_int()
    if (n >= 65 && n <= 90) ||
      (n >= 97 && n <= 122) ||
      (n >= 48 && n <= 57) ||
      n == 45 ||
      n == 46 ||
      n == 95 ||
      n == 126 ||
      n == 47 {
      out += n.unsafe_to_char().to_string()
    } else {
      out += "%" + hex[n / 16].to_string() + hex[n % 16].to_string()
    }
  }
  out
}

///|
pub fn propfind(
  raw_path : String,
  depth : Int,
  properties : Array[String],
) -> Request raise DavError {
  if depth != 0 && depth != 1 {
    raise Invalid("depth must be 0 or 1")
  }
  let mut body = ""
  if properties.is_empty() {
    body += ""
  } else {
    body += ""
    for p in properties {
      if p.is_empty() ||
        !p
        .to_array()
        .iter()
        .all(c => (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '-') {
        raise Invalid("DAV property name")
      }
      body += ""
    }
    body += ""
  }
  body += ""
  {
    verb: "PROPFIND",
    target: path(raw_path),
    headers: {
      "Depth": depth.to_string(),
      "Content-Type": "application/xml; charset=utf-8",
    },
    body,
  }
}

///|
pub fn copy_move(
  verb : String,
  raw_path : String,
  destination : String,
  overwrite : Bool,
) -> Request raise DavError {
  if verb != "COPY" && verb != "MOVE" {
    raise Invalid("COPY/MOVE required")
  }
  if (!destination.has_prefix("http://") && !destination.has_prefix("https://")) ||
    destination.contains("\r") ||
    destination.contains("\n") ||
    destination.contains(" ") {
    raise Invalid("absolute destination URI required")
  }
  {
    verb,
    target: path(raw_path),
    headers: {
      "Destination": destination,
      "Overwrite": if overwrite {
        "T"
      } else {
        "F"
      },
    },
    body: "",
  }
}

///|
pub fn property_xml(name : String, value : String) -> String raise DavError {
  xml_text(value)
  if name.is_empty() ||
    !name.to_array().iter().all(c => (c >= 'a' && c <= 'z') || c == '-') {
    raise Invalid("property name")
  }
  "" + escape(value) + ""
}