# Copyright (c) 2023 Yuki Kimoto
# MIT License

class Regex {
  version "0.258";
  
  use Resource::RE2;
  
  use Fn;
  use Regex::Replacer;
  use Regex::RE2;
  use Regex::Match;
  use Regex::ReplaceInfo;
  use Hash;
  use List;
  
  has re2 : Regex::RE2;
  
  static method new : Regex ($pattern : string, $flags : string = undef) {
    
    unless ($pattern) {
      die "The regex pattern \$pattern must be defined.";
    }
    
    my $self = new Regex;
    
    my $re2_pattern = &create_re2_pattern($pattern, $flags);;
    
    $self->compile($re2_pattern);
    
    return $self;
  }
  
  # Private class methods
  private static method create_re2_pattern : string ($pattern : string, $flags : string = undef) {
    
    my $re2_pattern = (string)undef;
    if ($flags) {
      $re2_pattern = "(?$flags)$pattern";
    }
    else {
      $re2_pattern = $pattern;
    }
    
    return $re2_pattern;
  }
  
  private native method compile : void ($pattern : string);
  
  # Instance Methods
  method match : Regex::Match ($string : string, $offset_ref : int* = undef, $length : int = -1) {
    
    my $string_offset = $offset_ref ? $$offset_ref : 0;
    my $match_offset = 0;
    my $regex_match = $self->match_string($string, $string_offset, \$match_offset, $length);
    if ($offset_ref) {
      $$offset_ref += $match_offset;
    }
    
    return $regex_match;
  }
  
  method replace : Regex::ReplaceInfo ($string : mutable string, $replace : string|Regex::Replacer, $offset_ref : int* = undef, $length : int = -1, $options : object[] = undef) {
    
    unless ($string) {
      die "The string \$string must be defined.";
    }
    
    my $string_buffer = StringBuffer->new_ref((mutable string)$string);
    
    my $options = Fn->to_hash($options);
    
    my $global = $options->{"global"}->(int);
    
    my $string_length = $string_buffer->length;
    
    my $string_offset = $offset_ref ? $$offset_ref : 0;
    
    my $match_offset = 0;
    
    my $regex_match = (Regex::Match)undef;
    my $match_count = 0;
    while (1) {
      
      $regex_match = $self->match_string($string_buffer->get_string, $string_offset, \$match_offset, $length);
      
      if ($regex_match) {
        $match_count++;
      }
      else {
        last;
      }
      
      my $replace_string : string;
      if ($replace isa string) {
        $replace_string = (string)$replace;
      }
      elsif ($replace isa Regex::Replacer) {
        my $replacer = (Regex::Replacer)$replace;
        $replace_string = $replacer->($self, $regex_match);
      }
      else {
        die "\$replace must be a string or a Regex::Replacer object";
      }
      
      my $replace_string_length = length $replace_string;
      
      my $match_start = $regex_match->match_start;
      my $match_length = $regex_match->match_length;
      
      $string_buffer->substr($match_start, $match_length, $replace_string);
      
      unless ($global) {
        last;
      }
      
      my $next_string_offset = $match_start + $replace_string_length;
      $match_offset = $next_string_offset - $string_offset;
      $length -= $match_offset;
    }
    
    my $replace_info = Regex::ReplaceInfo->new({replaced_count => $match_count, match => $regex_match});
    
    if ($offset_ref) {
      $$offset_ref += $match_offset;
    }
    
    if ($replace_info->replaced_count > 0) {
      return $replace_info;
    }
    else {
      return undef;
    }
  }
  
  method replace_g  : Regex::ReplaceInfo ($string : mutable string, $replace : string|Regex::Replacer, $offset_ref : int* = undef, $length : int = -1, $options : object[] = undef) {
    return $self->replace($string, $replace, $offset_ref, $length, Fn->merge_options($options, {global => 1}));
  }
  
  precompile method split : string[] ($string : string, $limit : int = 0) {
    
    unless ($string) {
      die "The string \$string must be defined";
    }
    
    my $string_length = length $string;
    
    my $parts = StringList->new;
    
    my $offset = 0;
    my $match_count = 0;
    for (my $i = 0; $i < $string_length; $i++) {
      if ($limit > 0 && $match_count >= $limit - 1) {
        last;
      }
      
      my $current_offset = $offset;
      my $regex_match = $self->match_string($string, 0, \$offset);
      if ($regex_match) {
        $match_count++;
        
        my $match_start = $regex_match->match_start;
        my $match_length = $regex_match->match_length;
        
        my $part = Fn->substr($string, $current_offset, $match_start - $current_offset);
        $parts->push($part);
        if (@{$regex_match->captures}) {
          $parts->push_($regex_match->captures);
        }
        
        $offset = $match_start + $match_length;
      }
    }
    if ($offset == $string_length) {
      $parts->push("");
    }
    else {
      my $part = Fn->substr($string, $offset, $string_length - $offset);
      $parts->push($part);
    }
    
    if ($limit == 0) {
      while (@$parts > 0) {
        if ($parts->[@$parts - 1] eq "") {
          $parts->pop;
        }
        else {
          last;
        }
      }
    }
    
    return $parts->get_array;
  }
  
  native method DESTROY : void ();
  
  # Private Instance Methods
  private native method match_string : Regex::Match ($string : string, $string_offset : int = 0, $match_offset_ref : int* = undef, $length : int = -1);
  
}