# Copyright (c) 2023 Yuki Kimoto
# MIT License

class Regex {
  version "0.247";
  
  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_or_buffer : object of string|StringBuffer, $offset_ref : int* = undef, $length : int = -1) {
    
    my $string = (string)undef;
    if ($string_or_buffer isa string) {
      $string = (string)$string_or_buffer;
    }
    elsif ($string_or_buffer isa StringBuffer) {
      
      my $string_buffer = (StringBuffer)$string_or_buffer;
      
      my $offset = 0;
      if ($offset_ref) {
        $offset = $$offset_ref;
      }
      
      if ($length < 0) {
        $length = $string_buffer->length - $offset;
      }
      
      unless ($offset + $length <= $string_buffer->length) {
        die "\$\$offset_ref + \$length must be less than or equalt to the lenght of \$string_or_buffer.";
      }
      
      $string = $string_buffer->get_string_unsafe;
    }
    else {
      die "The type of \$string_ref_or_buffer must be string or StringBuffer.";
    }
    
    my $regex_match = $self->match_string($string, $offset_ref, $length);
    
    return $regex_match;
  }
  
  method replace : Regex::ReplaceInfo ($string_ref_or_buffer : object of string[]|StringBuffer, $replace : object of string|Regex::Replacer, $offset_ref : int* = undef, $length : int = -1, $options : object[] = undef) {
    
    my $replace_info_ref = new Regex::ReplaceInfo[1];
    
    my $string_ref = (string)undef;
    if ($string_ref_or_buffer isa string[]) {
      
      my $string_ref = (string[])$string_ref_or_buffer;
      
      unless (@$string_ref == 1) {
        die "\$string_ref_or_buffer must be 1-length array if the type is string[].";
      }
      
      my $string = $string_ref->[0];
      
      $options = Fn->merge_options($options, {info => $replace_info_ref});
      
      my $replaced_string = $self->replace_string($string, $replace, $offset_ref, $length, $options);
      
      $string_ref->[0] = $replaced_string;
    }
    elsif ($string_ref_or_buffer isa StringBuffer) {
      
      my $string_buffer = (StringBuffer)$string_ref_or_buffer;
      
      my $offset = 0;
      if ($offset_ref) {
        $offset = $$offset_ref;
      }
      
      if ($length < 0) {
        $length = $string_buffer->length - $offset;
      }
      
      unless ($offset + $length <= $string_buffer->length) {
        die "\$\$offset_ref + \$length must be less than or equalt to the lenght of \$string_ref_or_buffer.";
      }
      
      $options = Fn->merge_options($options, {info => $replace_info_ref});
      
      $self->replace_buffer_common($string_buffer, $replace, $offset_ref, $length, $options);
    }
    else {
      die "The type of \$string_ref_or_buffer must be string[] or StringBuffer.";
    }
    
    my $replace_info = $replace_info_ref->[0];
    
    if ($replace_info->replaced_count > 0) {
      return $replace_info;
    }
    else {
      return undef;
    }
  }
  
  method replace_g  : Regex::ReplaceInfo ($string_ref_or_buffer : object of string[]|StringBuffer, $replace : object of string|Regex::Replacer, $offset_ref : int* = undef, $length : int = -1, $options : object[] = undef) {
    return $self->replace($string_ref_or_buffer, $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_list = StringList->new_len(0);
    
    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, \$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_list->push($part);
        $offset = $match_start + $match_length;
      }
    }
    if ($offset == $string_length) {
      $parts_list->push("");
    }
    else {
      my $part = Fn->substr($string, $offset, $string_length - $offset);
      $parts_list->push($part);
    }
    
    if ($limit == 0) {
      while ($parts_list->length > 0) {
        if ($parts_list->get($parts_list->length - 1) eq "") {
          $parts_list->pop;
        }
        else {
          last;
        }
      }
    }
    
    my $parts = $parts_list->to_array;
    
    return $parts;
  }
  
  native method DESTROY : void ();
  
  # Private Instance Methods
  private native method match_string : Regex::Match ($string : string, $offset_ref : int*, $length : int = -1);
  
  private method buffer_match : Regex::Match ($string_buffer : StringBuffer, $offset : int = 0, $length : int = -1) {
    if ($length < 0) {
      $length = $string_buffer->length - $offset;
    }
    
    my $regex_match = $self->match($string_buffer->get_string_unsafe, \$offset, $length);
    
    return $regex_match;
  }
  
  private method match_buffer : Regex::Match ($string_buffer : StringBuffer, $offset_ref : int*, $length : int = -1) {
    if ($length < 0) {
      $length = $string_buffer->length - $$offset_ref;
    }
    
    my $regex_match = $self->match_string($string_buffer->get_string_unsafe, $offset_ref, $length);
    
    return $regex_match;
  }
  
  private method replace_string : string ($string : string, $replace : object of string|Regex::Replacer, $offset_ref : int*, $length : int = -1, $options : object[] = undef) {
    my $string_buffer = StringBuffer->new;
    $string_buffer->push($string);
    
    $self->replace_buffer_common($string_buffer, $replace, $offset_ref, $length, $options);
    
    my $result_string = $string_buffer->to_string;
    
    return $result_string;
  }
  
  private precompile method replace_buffer_common : void ($string_buffer : StringBuffer, $replace : object of string|Regex::Replacer, $offset_ref : int*, $length : int = -1, $options : object[] = undef) {
    
    my $optiton_h = Hash->new($options);
    
    my $offset = 0;
    if ($offset_ref) {
      $offset = $$offset_ref;
    }
    
    my $original_offset = $offset;
    
    my $global = 0;
    if (my $global_obj = $optiton_h->get("global")) {
      $global = (int)$global_obj;
    }
    
    my $string_length = $string_buffer->length;
    
    if ($length == -1) {
      $length = $string_length - $offset;
    }
    
    my $regex_match = (Regex::Match)undef;
    my $match_count = 0;
    while (1) {
      $regex_match = $self->match_buffer($string_buffer, \$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->replace($match_start, $match_length, $replace_string);
      
      unless ($global) {
        last;
      }
      
      my $next_offset = $match_start + $replace_string_length;
      $offset = $next_offset;
      $length = $string_buffer->length - $offset;
    }
    
    my $regex_replace_info = Regex::ReplaceInfo->new({replaced_count => $match_count, match => $regex_match});
    
    my $option_regex_replace_info = (Regex::ReplaceInfo[])$optiton_h->get("info");
    
    if ($option_regex_replace_info) {
      $option_regex_replace_info->[0] = $regex_replace_info;
    }
    
    if ($offset_ref) {
      $$offset_ref = $offset;
    }
  }
  
}