# Copyright (c) 2023 Yuki Kimoto
# MIT License

class StringBuffer {
  version_from SPVM;
  use Fn;
  use Array;
  
  interface Cloneable;
  interface Comparable;
  interface EqualityCheckable;
  
  # Enumerations
  private enum {
    DEFAULT_CAPACITY = 4,
  }
  
  # Fields
  has capacity : ro int;
  
  has length : ro int;
  
  has string : mutable string;
  
  # Class methods
  static method new : StringBuffer ($string : string = undef, $capacity : int = -1) {
    my $length : int;
    if ($string) {
      $length = length $string;
    }
    else {
      $length = 0;
    }
    
    my $self = &new_len($length, $capacity);
    
    if ($string) {
      Fn->memcpy($self->{string}, 0, $string, 0, $length);
    }
    
    return $self;
  }
  
  static method new_len : StringBuffer ($length : int, $capacity : int = -1) {
    
    unless ($length >= 0) {
      die "The length \$length must be greater than or equal to 0.";
    }
    
    if ($capacity < 0) {
      $capacity = &DEFAULT_CAPACITY;
    }
    
    if ($length > $capacity) {
      $capacity = $length;
    }
    
    my $self = new StringBuffer;
    $self->{string} = (mutable string)new_string_len($capacity);
    $self->{capacity} = $capacity;
    $self->{length} = $length;
    
    return $self;
  }
  
  # Instance methods
  method push : void ($string : string, $offset : int = 0, $length : int = -1) {
    
    unless ($string) {
      die "The string \$string must be defined.";
    }
    my $string_length = length $string;
    
    unless ($offset >= 0) {
      die "The offset \$offset must be greater than or equal to 0.";
    }
    
    if ($length == -1) {
      $length = length $string - $offset;
    }
    
    unless ($offset + $length <= $string_length) {
      die "The offset \$offset + the length \$length must be less than or equal to the length of the string $string.";
    }
    
    my $buffer_length = $self->{length};
    my $new_length = $buffer_length + $length;
    $self->_maybe_extend($new_length);
    
    Fn->memcpy($self->{string}, $buffer_length, $string, $offset, $length);
    
    $self->{length} += $length;
  }
  
  method push_char : void ($char : int) {
    my $length = $self->{length};
    
    my $new_length = $length + 1;
    $self->_maybe_extend($new_length);
    
    $self->{string}[$self->{length}++] = (byte)$char;
  }
  
  method replace : void ($offset : int, $remove_length : int, $replace : string) {
    unless ($offset >= 0) {
      die "The offset \$offset must be greater than or equal to 0.";
    }
    
    unless ($remove_length >= 0) {
      die "The removal length \$remove_length must be greater than or equal to 0.";
    }
    unless ($offset + $remove_length <= $self->{length}) {
      die "\$offset + \$removing length must be less than or equal to the length of \$string buffer.";
    }
    
    my $replace_length = 0;
    if ($replace) {
      $replace_length = length $replace;
    }
    
    my $new_length = $self->{length} - $remove_length + $replace_length;
    $self->_maybe_extend($new_length);
    
    my $move_length = $self->{length} - $offset - $remove_length;
    Fn->memmove($self->{string}, $offset + $replace_length, $self->{string}, $offset + $remove_length, $move_length);
    
    if ($replace) {
      Fn->memcpy($self->{string}, $offset, $replace, 0, $replace_length);
    }
    
    $self->{length} = $new_length;
  }
  
  method reserve : void ($new_capacity : int) {
    unless ($new_capacity >= 0) {
      die "The new capacity \$new_capacity must be greater than or equal to 0.";
    }
    
    my $capacity = $self->{capacity};
    
    if ($new_capacity > $capacity) {
      my $length = $self->{length};
      my $new_buffer = (mutable string)new_string_len $new_capacity;
      Fn->memcpy($new_buffer, 0, $self->{string}, 0, $length);
      $self->{string} = $new_buffer;
      $self->{capacity} = $new_capacity;
    }
  }
  
  method to_string : string () {
    my $new_string = Fn->substr($self->{string}, 0, $self->{length});
    return $new_string;
  }
  
  method get_string_unsafe : string () {
    return $self->{string};
  }
  
  private method _maybe_extend : void ($min_capacity : int) {
    my $capacity = $self->{capacity};
    
    unless ($min_capacity > $capacity) {
      return;
    }
    
    if ($capacity < $min_capacity) {
      $capacity = $min_capacity;
    }
    
    my $new_capacity = $capacity * 2;
    my $new_buffer = (mutable string)new_string_len $new_capacity;
    
    my $length = $self->{length};
    my $buffer = $self->{string};
    
    Fn->memcpy($new_buffer, 0, $buffer, 0, $length);
    
    $self->{string} = $new_buffer;
    $self->{capacity} = $new_capacity;
  }
  
  method set_length : void ($length : int) {
    
    unless ($length >= 0) {
      die "The length \$length must be greater than or equal to 0.";
    }
    
    $self->_maybe_extend($length);
    
    $self->{length} = $length;
    
    if ($length < $self->{capacity}) {
      Fn->memset_char($self->{string}, '\0', $length, $self->{capacity} - $length);
    }
    
  }
  
  method set : void ($string : string) {
    
    $self->set_length(0);
    
    $self->push($string);
  }
  
  method clone : StringBuffer () {
    
    my $string = $self->to_string;
    
    my $clone = &new($string);
    
    return $clone;
  }
  
  method cmp : int ($a : StringBuffer, $b : StringBuffer) {
    
    my $a_length = -1;
    my $a_string = (string)undef;
    
    if ($a) {
      $a_length = $a->{length};
      $a_string = $a->{string};
    }
    
    my $b_length = -1;
    my $b_string = (string)undef;
    
    if ($b) {
      $b_length = $b->{length};
      $b_string = $b->{string};
    }
    
    my $cmp = &cmp_common($a_string, $a_length, $b_string, $b_length);
    
    return $cmp;
  }
  
  private static method cmp_common : int ($a_string : string, $a_length : int, $b_string : string, $b_length : int) {
    
    my $cmp = 0;
    
    if ($a_string == undef && $b_string == undef) {
      $cmp = 0;
    }
    elsif ($a_string != undef && $b_string == undef) {
      $cmp = 1;
    }
    elsif ($a_string == undef && $b_string != undef) {
      $cmp = -1;
    }
    else {
      my $short_length = -1;
      
      if ($a_length < $b_length) {
        $short_length = $a_length;
      }
      else {
        $short_length = $b_length;
      }
      
      my $memcmp_ret = Fn->memcmp($a_string, 0, $b_string, 0, $short_length);
      
      if ($memcmp_ret) {
        if ($memcmp_ret < 0) {
          $cmp = -1;
        }
        else {
          $cmp = 1;
        }
      }
      elsif ($a_length == $b_length) {
        $cmp = 0;
      }
      else {
        if ($a_length < $b_length) {
          $cmp = -1;
        }
        else {
          $cmp = 1;
        }
      }
    }
    
    return $cmp;
  }
  
  method eq : int ($a : StringBuffer, $b : StringBuffer) {
    
    my $eq = 0;
    
    if ($a && $b) {
      $eq = $a->cmp($a, $b) == 0;
    }
    elsif ($a) {
      $eq = 0;
    }
    elsif ($b) {
      $eq = 0;
    }
    else {
      $eq = 1;
    }
    
    return $eq;
  }
  
  method compare_string : int ($string : string) {
    
    my $a_length = $self->{length};
    my $a_string = $self->{string};
    
    my $b_length = -1;
    my $b_string = (string)undef;
    
    if ($string) {
      $b_length = length $string;
      $b_string = $string;
    }
    
    my $cmp = &cmp_common($a_string, $a_length, $b_string, $b_length);
    
    return $cmp;
  }
  
  method equals_string : int ($string : string) {
    
    my $cmp = $self->compare_string($string);
    
    my $eq = ($cmp == 0);
    
    return $eq;
  }
  
  method substr : string ($offset : int, $length : int = -1) {
    
    my $string = $self->{string};
    
    my $string_length = $self->{length};
    
    unless ($offset + $length <= $string_length) {
      die "The offset \$offset + the length \$length must be less than or equal to the value of length field.";
    }
    
    my $substr = Fn->substr($string, $offset, $length);
    
    return $substr;
  }
  
  method index : int ($substring : string, $begin : int = 0, $end : int = -1) {
    
    my $string = $self->{string};
    
    my $string_length = $self->{length};
    
    unless ($begin >= 0 && $begin <= $string_length) {
      die "The begin \$begin must be between 0 and the length of string field.";
    }
    
    if ($end < 0) {
      $end = $string_length;
    }
    
    unless ($end <= $string_length) {
      die "The end \$end must be less than or equal to the length of string field.";
    }
    
    my $ret = Fn->index($string, $substring, $begin, $end);
    
    return $ret;
  }
  
  method contains : int ($substring : string, $begin : int = 0, $end : int = -1) {
    return $self->index($substring, $begin, $end) >= 0;
  }
  
}