package SPVM::HTTP::URL::Escape {

  private sub HEX_CHARS : string () {
    return "0123456789ABCDEF";
  }

  private sub is_unreserved : int ($c : byte) {
    # RFC3986
    return ('A' <= $c && $c <= 'Z') ||
      ('a' <= $c && $c <= 'z') ||
      ('0' <= $c && $c <= '9') ||
      $c == '-' ||
      $c == '.' ||
      $c == '_' ||
      $c == '~';
  }

  private sub is_sub_delim : int ($c : byte) {
    return $c == '!' || $c == '$' || $c == '&' ||
      $c == '\'' || $c == '(' || $c == ')' ||
      $c == '*' || $c == '+' || $c == ',' ||
      $c == ';' || $c == '=';
  }

  private sub hex_index : int ($c : byte) {
    if ('0' <= $c && $c <= '9') {
      return $c - '0';
    }
    elsif ('A' <= $c && $c <= 'F') {
      return $c - 'A' + 10;
    }
    else {
      die "string is broken. invalid hex-digit. c = '" . [$c] . "'";
    }
  }

  sub escape : string ($str : string) {
    my $res = "";
    my $length = length $str;
    for (my $i = 0; $i < $length; ++$i) {
      my $c : int = $str->[$i] & 0xFF;
      if (is_unreserved((byte)$c) || is_sub_delim((byte)$c)) {
        $res .= [(byte)$c];
      } else {
        $res .= ['%', HEX_CHARS()->[(int)($c / 16)], HEX_CHARS()->[$c % 16]];
      }
    }
    return $res;
  }

  sub unescape : string ($str : string) {
    my $res = "";
    my $length = length $str;
    for (my $i = 0; $i < $length; ++$i) {
      if (is_unreserved($str->[$i]) || is_sub_delim($str->[$i])) {
        $res .= [$str->[$i]];
      }
      else {
        unless ($str->[$i] == '%') {
          die "string is broken. '%' is expected. str: $str";
        }
        unless ($i + 2 < $length) {
          die "string is broken. two hex-digits are required after '%'. str: $str";
        }
        $res .= [(byte)(hex_index($str->[$i + 1]) * 16 + hex_index($str->[$i + 2]))];
        $i += 2;
      }
    }
    return $res;
  }
}

=pod

=head1 NAME

SPVM::HTTP::URL::Escape - Percent-encode and percent-decode unsafe characters

=cut
