# Copyright (c) 2025 Yuki Kimoto
# MIT License

class Mojolicious::Routes::Route {
  version_from Mojolicious;
  
  use Mojolicious::Routes;
  use Mojolicious::Routes::Pattern;
  use Mojolicious::Callback;
  
  # Class Variables
  # Reserved stash values
  our $RESERVED : Hash of string;
  INIT {
    
    my $reserved = [
      "action",
      "app",
      "cb",
      "controller",
      "data",
      "extends",
      "format",
      "handler",
      "inline",
      "json",
      "layout",
      "namespace",
      "path",
      "status",
      "template",
      "text",
      "variant",
    ];
    
    $RESERVED = Hash->new_from_keys($reserved => 1);
  }
  
  # Fields
  has pattern : rw Mojolicious::Routes::Pattern;
  
  has methods : rw string[];
  
  has inline : rw byte;
  
  has websocket : byte;
  
  has children : rw Mojolicious::Routes::Route[];
  
  has parent : rw Mojolicious::Routes::Route
    set {
      $self->{parent} = $_;
      weaken $self->{parent};
    }
  ;
  
  has cb : rw Mojolicious::Callback;
  
  has name : rw string;
  
  # Undocumented Fields
  
  # Class Methods
  static method new : Mojolicious::Routes::Route () {
    
    my $self = new Mojolicious::Routes::Route;
    
    $self->init;
    
    return $self;
  }
  
  # Instance Methods
  protected method init : void ($options : object[] = undef) {
    
    $self->{children} = new Mojolicious::Routes::Route[0];
    
    $self->{pattern} = Mojolicious::Routes::Pattern->new;
    
  }
  
  method any : Mojolicious::Routes::Route ($args : object...) {
    
    my $methods = (string[])undef;
    my $shift = 0;
    if ($args->[0] isa string[]) {
      $methods = (string[])$args->[0];
      $shift = 1;
    }
    
    $args = $shift ? (object...)Array->copy_object_address($args, 1, @$args - 1) : $args;
    
    return $self->_generate_route($methods, $args);
  }
  
  method delete : Mojolicious::Routes::Route ($args : object...) {
    return $self->_generate_route(DELETE => $args);
  }
  
  method get : Mojolicious::Routes::Route ($args : object...) {
    return $self->_generate_route(GET => $args);
  }
  
  method options : Mojolicious::Routes::Route ($args : object...) {
    return $self->_generate_route(OPTIONS => $args);
  }
  
  method patch : Mojolicious::Routes::Route ($args : object...) {
    return $self->_generate_route(PATCH => $args);
  }
  
  method post : Mojolicious::Routes::Route ($args : object...) {
    return $self->_generate_route(POST => $args);
  }
  
  method put : Mojolicious::Routes::Route ($args : object...) {
    return $self->_generate_route(PUT => $args);
  }
  
  method under : Mojolicious::Routes::Route ($args : object...) {
    return $self->_generate_route(under => $args);
  }
  
  method websocket : Mojolicious::Routes::Route ($args : object...) {
    
    my $route = $self->get($args);
    
    $route->{websocket} = 1;
    
    return $route;
  }
  
  method root : Mojolicious::Routes () {
    
    return (Mojolicious::Routes)$self->_chain->[0];
  }
  
  method is_websocket : int () {
    
    return !!$self->{websocket};
  }
  
  method is_reserved : int ($name : string) {
    return !!$RESERVED->get($name);
  }
  
  method is_endpoint : int () {
    
    return $self->inline ? 0 : !@{$self->children};
  }
  
  method parse : void ($pattern_string : string) {
    
    my $pattern = $self->pattern;
    
    $pattern->parse($pattern_string);
  }
  
  method to : void ($defaults : Hash)  {
    
    my $pattern_defaults = $self->pattern->defaults;
    
    for my $key (@{$defaults->keys}) {
      my $value = $defaults->{$key};
      $pattern_defaults->{$key} = $value;
    }
  }
  
  method to_string : string () {
    
    my $chain = $self->_chain;
    
    my $pattern_unparseds = StringList->new;
    
    for (my $i = 0; $i < @$chain; $i++) {
      my $chain = $chain->[$i];
      
      my $pattern_unparsed = $chain->pattern->unparsed // "";
      
      $pattern_unparseds->push($pattern_unparsed);
    }
    
    return Fn->join("", $pattern_unparseds->to_array);
  }
  
  method has_websocket : int () {
    my $chain = $self->_chain;
    
    my $has_websocket = 0;
    for (my $i = 0; $i < @$chain; $i++) {
      my $child = $chain->[$i];
      
      if ($child->is_websocket) {
        $has_websocket = 1;
      }
    }
    return $has_websocket;
  }
  
  private method add_child : void ($route : Mojolicious::Routes::Route)  {
    
    $route->remove;
    
    $route->set_parent($self);
    
    my $children = (List of Mojolicious::Routes::Route)List->new_ref($self->{children});
    
    $children->push($route);
  }
  
  private method remove : void () {
    
    my $parent = $self->parent;
    
    unless ($parent) {
      return;
    }
    
    my $new_children = (List of Mojolicious::Routes::Route)List->new(new Mojolicious::Routes::Route[0]);
    for (my $i = 0; $i < @{$parent->children}; $i++) {
      my $child = $parent->children->[$i];
      
      unless ($child == $self) {
        $new_children->push($child);
      }
    }
    $parent->set_children($new_children->get_array);
    
    $self->set_parent(undef);
  }
  
  private method _chain : List of Mojolicious::Routes::Route () {
    
    my $chain = (List of Mojolicious::Routes::Route)List->new([$self]);
    
    while ($self = $self->parent) {
      $chain->unshift($self);
    }
    
    return $chain;
  }
  
  private method _generate_route : Mojolicious::Routes::Route ($methods : string|string[], $args : object...) {
    
    my $args = List->new($args);
    
    my $defaults = Hash->new;
    
    my $template = (string)undef;
    
    my $pattern = (string)undef;
    
    my $cb = (Mojolicious::Callback)undef;
    
    while (1) {
      
      unless (@$args) {
        last;
      }
      
      my $arg = $args->shift;
      
      # First scalar is the pattern
      if ($arg isa string && !$pattern) {
        $pattern = (string)$arg;
      }
      # Scalar
      elsif ($arg isa string && @$args) {
        die "Condition is not supported.";
      }
      # Last scalar is the template
      elsif ($arg isa string) {
        $template = (string)$arg;
      }
      # Callback
      elsif ($arg isa Mojolicious::Callback) {
        $cb = (Mojolicious::Callback)$arg;
      }
      # Defaults
      elsif (is_options $arg || $arg isa Hash) {
        $defaults = Fn->to_hash($arg);
      }
      
      # Constraints
      elsif ($arg isa object[]) {
        die "Constraint is not supported.";
      }
      else {
        die "Invalid routing argument.";
      }
    }
    
    my $route = $self->_route($pattern);
    
    if ($methods isa string && $methods->(string) eq "under") {
      $route->set_inline(1);
    }
    else {
      if ($methods isa string) {
        $methods = [(string)$methods];
      }
      $route->set_methods((string[])$methods);
    }
    
    $route->pattern->defaults->{"template"} = $self->{name} = $template;
    
    if ($cb) {
      $route->set_cb($cb);
    }
    
    $route->to($defaults);
    
    return $route;
  }
  
  private method _route : Mojolicious::Routes::Route ($pattern : string) {
    
    my $r = Mojolicious::Routes::Route->new;
    
    $r->parse($pattern);
    
    $self->add_child($r);
    
    my $pattern = $r->pattern;
    
    my $has_reserved = 0;
    for (my $i = 0; $i < @{$pattern->placeholders}; $i++) {
      my $placeholder = $pattern->placeholders->[$i];
      if ($self->is_reserved($placeholder)) {
        $has_reserved = 1;
        last;
      }
    }
    if ($has_reserved) {
      die "Route pattern " . $pattern->unparsed . " contains a reserved stash value.";
    }
    
    return $r;
  }

}