# Copyright (c) 2023 Yuki Kimoto
# MIT License

class Mojo::URL {
  version_from Mojo;
  
  use StringBuffer;
  use Re;
  use Mojo::URL;
  use Mojo::Parameters;
  use Mojo::Util;
  
  # Fields
  has fragment : rw string;
  
  has scheme : rw string;
  
  has userinfo : rw string;
  
  has host : rw string;
  
  has port : rw int;
  
  has path : Mojo::Path;
  
  has auth : rw string;
  
  has base : rw Mojo::URL;
  
  has query : Mojo::Parameters;
  
  static method new : Mojo::URL ($url : string = undef) {
    
    my $self = new Mojo::URL;
    
    if ($url) {
      $self->parse($url);
    }
    
    return $self;
  }
  
  method clone : Mojo::URL () {
    
    my $clone = Mojo::URL->new;
    
    $clone->{fragment} = $self->{fragment};
    $clone->{host} = $self->{host};
    $clone->{port} = $self->{port};
    $clone->{scheme} = $self->{scheme};
    $clone->{userinfo} = $self->{userinfo};
    
    if ($self->{base}) {
      $clone->{base} = $self->{base}->clone;
    }
    
    if ($self->{path}) {
      $clone->{path} = $self->{path}->clone;
    }
    
    if ($self->{query}) {
      $clone->{query} = $self->{query}->clone;
    }
    
    return $clone;
  }
  
  method host_port : string () {
    
    my $host = $self->ihost;
    unless ($host) {
      return undef ;
    }
    
    my $port = $self->port;
    unless ($port) {
      return $host;
    }
    
    return "$host:$port";
  }
  
  method set_host_port : void ($host_port : string) {
    
    my $host_port_ref = [$host_port];
    if (my $replace_info = Re->s($host_port_ref, ":(\d+)$", "")) {
      my $match = $replace_info->match;
      $self->set_port((int)$match->cap1);
    }
    $host_port = $host_port_ref->[0];
    
    my $host = Mojo::Util->url_unescape($host_port);
    
    if (Re->m($host, "[^\x00-\x7f]")) {
      $self->set_ihost($host);
    }
    else {
      $self->set_host($host);
    }
  }
  
  method ihost : string () {
    
    my $host = $self->host;
    unless ($host) {
      return undef;
    }
    
    unless (Re->m($host, "[^\x00-\x7f]")) {
      return $host;
    }
    
    my $parts = Fn->split(".", $host, -1);
    
    my $parts_punycode_encoded = (string[])Fn->map(method : string ($part : string) {
      if (my $match = Re->m($part, "[^\x00-\x7f]")) {
        return  "xn--" . Mojo::Util->punycode_encode($match->cap1);
      }
      else {
        return $part;
      }
    }, $parts);
    
    my $ihost = Fn->join(".", $parts_punycode_encoded);
    
    return $ihost;
  }
  
  method set_ihost : void ($ihost : string) {
    
    my $parts = Fn->split(".", $ihost, -1);
    
    my $parts_punycode_decoded = (string[])Fn->map(method : string ($part : string) {
      if (my $match = Re->m($part, "^xn--(.+)$")) {
        return  Mojo::Util->punycode_decode($match->cap1);
      }
      else {
        return $part;
      }
    }, $parts);
    
    my $host = Fn->join(".", $parts_punycode_decoded);
    
    $self->set_host($host);
  }
  
  method is_abs : int () {
    return !!$self->scheme;
  }
  
  method parse : void ($url : string) {
    
    # Official regex from RFC 3986
    if (my $match = Re->m($url, "^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?")) {
      
      if ($match->cap2) {
        $self->set_scheme($match->cap2);
      }
      
      if ($match->cap5) {
        $self->set_path(Mojo::Path->new($match->cap5));
      }
      
      if ($match->cap7) {
        $self->set_query(Mojo::Parameters->new($match->cap7));
      }
      
      if ($match->cap9) {
        $self->set_fragment(Mojo::Util->url_unescape($match->cap9));
      }
      
      if (my $auth = $match->cap4) {
        
        my $auth_ref = [$auth];
        if (my $replace_info = Re->s($auth_ref, "^([^\@]+)\@", "")) {
          my $match = $replace_info->match;
          $self->set_userinfo(Mojo::Util->url_unescape($match->cap1)) ;
        }
        $auth = $auth_ref->[0];
        
        $self->set_host_port($auth);
      }
    }
  }

  method password : string () {
    
    my $userinfo = "";
    if ($self->userinfo) {
      $userinfo = $self->userinfo;
    }
    
    my $password = (string)undef;
    if (my $match = Re->m($userinfo, ":(.*)$")) {
      $password = $match->cap1;
    }
    
    return $password;
  }
  
  method path : Mojo::Path () {
    
    unless ($self->{path}) {
      $self->{path} = Mojo::Path->new;
    }
    
    my $path = $self->{path};
    
    return $path;
  }
  
  method set_path : void ($path : object of string|Mojo::Path) {
    
    unless ($path) {
      die "The path \$path must be defined.";
    }
    
    if ($path isa Mojo::Path) {
      $self->{path} = (Mojo::Path)$path;
    }
    else {
      unless ($self->{path}) {
        $self->{path} = Mojo::Path->new;
      }
      
      $self->{path}->merge((string)$path);
    }
    
  }
  
  method path_query : string () {
    
    my $query = $self->query->to_string;
    
    my $path_query = $self->path->to_string;
    
    if (length $query) {
      $path_query .= "?$query";
    }
    
    return $path_query;
  }
  
  method set_path_query : void ($pq : string) {
    
    if (my $match = Re->m($pq, "^([^?#]*)(?:\?([^#]*))?")) {
      if ($match->cap2) {
        $self->set_path(Mojo::Path->new($match->cap1));
        $self->set_query(Mojo::Parameters->new($match->cap2));
      }
      else {
        $self->set_path(Mojo::Path->new($match->cap1));
      }
    }
    
  }
  
  method protocol : string () {
    
    my $protocol = "";
    if (my $scheme = $self->scheme) {
      $protocol = Fn->lc($scheme);
    }
    
    return $protocol;
  }
  
  method query : Mojo::Parameters () {
    
    unless ($self->{query}) {
      $self->{query} = Mojo::Parameters->new;
    }
    
    return $self->{query};
  }
  
  method set_query : void ($query : object of string[]|Hash|Mojo::Parameters)  {
    
    unless ($query) {
      die "The query \$query must be defined.";
    }
    
    unless ($self->{query}) {
      $self->{query} = Mojo::Parameters->new;
    }
    
    # Merge with hash
    if ($query isa Hash) {
      $self->{query}->merge($query->(Hash)->to_array);
    }
    
    # Append array
    elsif ($query isa string[]) {
      $self->{query}->append((string[])$query);
    }
    
    # New parameters
    elsif ($query isa Mojo::Parameters) {
      $self->{query} = (Mojo::Parameters)$query;
    }
    
  }
  
  method to_abs : Mojo::URL ($base : Mojo::URL = undef) {

    my $abs = $self->clone;
    
    if ($abs->is_abs) {
      return $abs;
    }
    
    # Scheme
    unless ($base) {
      $base = $abs->base;
    }
    $abs->set_base($base);
    $abs->set_scheme($base->scheme);
    
    # Authority
    if ($abs->host) {
      return $abs;
    }
    $abs->set_userinfo($base->userinfo);
    $abs->set_host($base->host);
    $abs->set_port($base->port);
    
    # Absolute path
    my $path = $abs->path;
    if ($path->leading_slash) {
      return $abs;
    }
    
    # Inherit path
    if (!@{$path->parts}) {
      my $abs_path = $base->path->clone;
      $abs_path->canonicalize;
      $abs->set_path($abs_path);
      
      # Query
      unless (length $abs->query->to_string) {
        $abs->set_query($base->query->clone);
      }
    }
    
    # Merge paths
    else {
      my $abs_path = $base->path->clone;
      $abs_path->merge($path);
      $abs_path->canonicalize;
      $abs->set_path($abs_path);
    }
    
    return $abs;
  }

  method to_string : string () {
    return $self->_string(0);
  }
  
  method to_unsafe_string : string () {
    return $self->_string(1);
  }
  
  method _string : string ($unsafe : int) {
    
    # Scheme
    my $url = "";
    if (my $proto = $self->protocol) {
      $url .= "$proto:";
    }
    
    # Authority
    my $auth = $self->host_port;
    if ($auth) {
      $auth = Mojo::Util->url_escape($auth, q'^A-Za-z0-9\\-._~!$&\'()*+,;=:\\[\\]');
    }
    
    my $info = $self->userinfo;
    if ($unsafe && $info) {
      $auth = Mojo::Util->url_escape(q'^A-Za-z0-9\\-._~!$&\'()*+,;=:') . "@" . $auth;
    }
    
    if ($auth) {
      $url .= "//$auth";
    }
    
    # Path and query
    my $path = $self->path_query;
    if (!$auth || !length $path || Re->m($path, "^[/?]")) {
      $url .= $path;
    }
    else {
      $url .= "/$path";
    }
    
    # Fragment
    my $fragment = $self->fragment;
    unless (length $fragment) {
      return $url;
    }
    
    $url .= "#" . Mojo::Util->url_escape($fragment, q'^A-Za-z0-9\\-._~!$&\'()*+,;=:@/?');
    
    return $url;
  }
  
  method username : string () {
    
    my $userinfo = "";
    if ($self->userinfo) {
      
    }
    
    my $username = (string)undef;
    if (my $match = Re->m($username, "^([^:]+)")) {
      $username = $match->cap1;
    }
    
    return $username;
  }

}
