package SPVM::HTTP::URL {
  use SPVM::HTTP::URL::Escape;
  use SPVM::HTTP::URL::Parameters;

  # [scheme:][//[host][/path][?query][#fragment]
  has scheme    : public string;
  has host      : public string;
  has port      : public int;
  has path      : public string;
  has params    : public SPVM::HTTP::URL::Parameters;
  has fragment  : public string;

  private sub parse_scheme : int ($self : self, $url : string) {
    my $length = length $url;
    for (my $i = 0; $i < $length; ++$i) {
      if ($url->[$i] == ':') {
        $self->{scheme} = sliceb((byte [])$url, 0, $i);
        return $i + 1;
      }
    }
    die "Cannot parse scheme: not found ':'";
  }

  private sub parse_host : int ($self : self, $url : string, $start : int) {
    my $length = length $url;
    unless ($start + 2 < $length &&
        $url->[$start] == '/' && $url->[$start + 1] == '/') {
      die "Cannot parse host: requires \"//\"";
    }
    $start += 2;
    for (my $i = $start; $i < $length; ++$i) {
      if ($url->[$i] == '/' ||
          $url->[$i] == '?' ||
          $url->[$i] == '#') {
        $self->{host} = sliceb((byte [])$url, $start, $i - $start);
        return $i;
      } elsif ($url->[$i] == ':') {
        $self->{host} = sliceb((byte [])$url, $start, $i - $start);
        return $self->parse_port($url, $i + 1);
      }
    }
    $self->{host} = sliceb((byte [])$url, $start, $length - $start);
    return $length;
  }

  private sub parse_port : int ($self : self, $url : string, $start : int) {
    my $length = length $url;
    my $port = 0;
    for (my $i = $start; $i < $length; ++$i) {
      if ($url->[$i] == '/' ||
          $url->[$i] == '?' ||
          $url->[$i] == '#') {
        $self->{port} = $port;
        return $i;
      } elsif ('0' <= $url->[$i] && $url->[$i] <= '9') {
        $port = $port * 10 + ($url->[$i] - '0');
      } else {
        die "Cannot parse port: port contains non-digit character";
      }
    }
    $self->{port} = $port;
    return $length;
  }

  private sub parse_path : int ($self : self, $url : string, $start : int) {
    my $length = length $url;
    unless ($start < $length && $url->[$start] == '/') {
      return $start;
    }
    $self->{path} = "/";
    ++$start;
    for (my $i = $start; $i < $length; ++$i) {
      if ($url->[$i] == '/') {
        $self->{path} .= SPVM::HTTP::URL::Escape->unescape(
          sliceb((byte [])$url, $start, $i - $start)) . "/";
        $start = $i + 1;
      }
      elsif ($url->[$i] == '?' ||
          $url->[$i] == '#') {
        $self->{path} .= SPVM::HTTP::URL::Escape->unescape(
          sliceb((byte [])$url, $start, $i - $start));
        return $i;
      }
    }
    if ($start < $length) {
      $self->{path} .= SPVM::HTTP::URL::Escape->unescape(
        sliceb((byte [])$url, $start, $length - $start));
    }
    return $length;
  }

  private sub parse_params : int ($self : self, $url : string, $start : int) {
    my $length = length $url;
    unless ($start < $length && $url->[$start] == '?') {
      return $start;
    }
    for (my $i = $start + 1; $i < $length; ++$i) {
      if ($url->[$i] == '#') {
        $self->{params} = SPVM::HTTP::URL::Parameters->parse(
          sliceb((byte [])$url, $start + 1, $i - $start - 1)
        );
        return $i;
      }
    }
    $self->{params} = SPVM::HTTP::URL::Parameters->parse(
      sliceb((byte [])$url, $start + 1, $length - $start - 1)
    );
    return $length;
  }

  private sub parse_fragment : void ($self : self, $url : string, $start : int) {
    my $length = length $url;
    unless ($url->[$start] == '#') {
      return;
    }
    # https://stackoverflow.com/questions/2849756/list-of-valid-characters-for-the-fragment-identifier-in-an-url
    $self->{fragment} = SPVM::HTTP::URL::Escape->unescape(
        sliceb((byte [])$url, $start + 1, $length - $start - 1));
  }

  private sub parse_url : void ($self : self, $url : string) {
    my $length = length $url;
    my $next = $self->parse_scheme($url);
    $next = $self->parse_host($url, $next);
    if ($next == $length) {
      return;
    }
    $next = $self->parse_path($url, $next);
    if ($next == $length) {
      return;
    }
    $next = $self->parse_params($url, $next);
    if ($next == $length) {
      return;
    }
    $self->parse_fragment($url, $next);
  }

  sub parse : SPVM::HTTP::URL ($url : string) {
    my $self = new SPVM::HTTP::URL;

    # set default values
    $self->{scheme} = "";
    $self->{host} = "";
    $self->{port} = 80;
    $self->{path} = "";
    $self->{params} = SPVM::HTTP::URL::Parameters->new();
    $self->{fragment} = "";

    $self->parse_url($url);

    return $self;
  }

  sub has_trailing_slash : int ($self : self) {
    my $length = length($self->{path});
    return $length && $self->{path}->[$length - 1] == '/';
  }

  sub force_trailing_slash : void ($self : self) {
    if (!$self->has_trailing_slash) {
      $self->{path} .= "/";
    }
  }

  sub remove_trailing_slash : void ($self : self) {
    if ($self->has_trailing_slash) {
      $self->{path} = (string)sliceb(
        (byte[])$self->{path}, 0, length($self->{path}) - 1);
    }
  }

  sub root_path_slashed : int ($self : self) {
    return length($self->{path}) && $self->{path}->[0] == '/';
  }

  sub force_root_path_slashed : void ($self : self) {
    if (length $self->{path}) {
      unless ($self->{path}->[0] == '/') {
        $self->{path} = "/" . $self->{path};
      }
    }
    else {
      $self->{path} = "/";
    }
  }

  sub to_str : string ($self : self) {
    my $url = copy_str($self->{scheme}) . ":";
    $url .= "//" . $self->{host}; # TODO: escape by punycode if needed
    if ($self->{port} != 80) { # "host:80" is default
      $url .= ":" . $self->{port};
    }
    $url .= $self->{path};
    if (@{$self->{params}->keys}) {
      $url .= "?" . $self->{params}->to_str;
    }
    if (length($self->{fragment})) {
      $url .= "#" . SPVM::HTTP::URL::Escape->escape($self->{fragment});
    }
    return $url;
  }
}

=pod

=head1 NAME

SPVM::HTTP::URL - Uniform Resource Locators

=head1 SYNOPSIS

my $url = SPVM::HTTP::URL->parse($url_escaped);
my $scheme = $url->{scheme};
my $host = $url->{host};
my $port : int = $url->{port};
my $path = $url->{path};
eval {
  my $query_p = $url->{params}->get("p");
};
my $fragment = $url->{fragment};

=cut
