package SPVM::List : private {
  has values   : private object[];
  has capacity : private int;
  has offset   : private int; # range of offset: [-capacity, -1)
  has length   : private int;
  
  sub to_iarray : int[] ($self : self) {
    my $length = $self->{length};
    my $nums = new int[$length];
    for (my $i = 0; $i < $length; $i++) {
      my $values = $self->{values};
      my $value = $values->[$i];
      if ($value isa SPVM::Int) {
        $nums->[$i] = (int)$value;
      }
      else {
        croak "Can't convert to int type element";
      }
    }
    return $nums;
  }

  sub _reallocate_with_capacity : void ($self : self, $new_capacity : int) {
    my $new_values = new object [$new_capacity];
    for (my $i = 0; $i < $self->{length}; ++$i) {
      my $target = $self->{offset} + $i;
      if ($target < 0) { $target += $self->{capacity}; };
      $new_values->[$i] = $self->{values}[$target];
    }
    $self->{offset} = -$new_capacity;
    $self->{capacity} = $new_capacity;
    $self->{values} = $new_values;
  }

  sub _reallocate : void ($self : self) {
    my $new_capacity : int;
    if ($self->{capacity} > INT32_MAX() / 2) {
      $new_capacity = INT32_MAX();
    }
    else {
      $new_capacity = $self->{capacity} * 2;
    }
    unless ($new_capacity) {
      $new_capacity = 1;
    }
    $self->_reallocate_with_capacity($new_capacity);
  }

  sub _values : object[] ($self : self) {
    return $self->{values};
  }

  sub _capacity : int ($self : self) {
    return $self->{capacity};
  }

  sub _offset : int ($self : self) {
    return $self->{offset};
  }

  sub new : SPVM::List () {
    my $self = new SPVM::List;
    my $default_capacity = 16;
    $self->{capacity} = $default_capacity;
    $self->{values} = new object [$default_capacity];
    $self->{offset} = -$default_capacity;
    return $self;
  }

  sub new_with_capacity : SPVM::List ($capacity : int) {
    my $self = new SPVM::List;
    unless ($capacity > 0) {
      croak "capacity must be positive";
    }
    $self->{capacity} = $capacity;
    $self->{values} = new object [$capacity];
    $self->{offset} = -$capacity;
    return $self;
  }

  sub new_with_array : SPVM::List ($objects : object[]) {
    my $self = new SPVM::List;
    $self->{capacity} = @$objects;
    $self->{values} = new object [@$objects];
    for (my $i = 0; $i < @$objects; ++$i) {
      $self->{values}[$i] = $objects->[$i];
    }
    $self->{length} = @$objects;
    $self->{offset} = -@$objects;
    return $self;
  }

  sub length : int ($self : self) {
    return $self->{length};
  }

  sub push : void ($self : self, $value : object) {
    if ($self->{length} + 1 > $self->{capacity}) {
      $self->_reallocate;
    }
    my $index = $self->{offset} + $self->{length};
    if ($index < 0) { $index += $self->{capacity}; }
    $self->{values}[$index] = $value;
    ++$self->{length};
  }

  sub pop : object ($self : self) {
    if ($self->{length} - 1 < 0) {
      return undef;
    }
    my $target = $self->{offset} + $self->{length} - 1 + $self->{capacity};
    my $ret = $self->{values}[$target];
    $self->{values}[$target] = undef;
    --$self->{length};
    return $ret;
  }

  sub unshift : void ($self : self, $value : object) {
    if ($self->{length} + 1 > $self->{capacity}) {
      $self->_reallocate;
    }
    if (--$self->{offset} < -$self->{capacity}) {
      $self->{offset} = -1;
    }
    $self->{values}[$self->{offset} + $self->{capacity}] = $value;
    ++$self->{length};
  }

  sub shift : object ($self : self) {
    if ($self->{length} - 1 < 0) {
      return undef;
    }
    my $ret = $self->{values}[$self->{offset} + $self->{capacity}];
    $self->{values}[$self->{offset} + $self->{capacity}] = undef;
    --$self->{length};
    if (++$self->{offset} > -1) {
      $self->{offset} = -$self->{capacity};
    }
    return $ret;
  }

  sub set : void ($self : self, $index : int, $value : object) {
    my $target = $self->{offset} + $index;
    if ($target < 0) { $target += $self->{capacity}; }
    else { $target %= $self->{capacity}; }
    $self->{values}[$target] = $value;
  }

  sub get : object ($self : self, $index : int) {
    my $target = $self->{offset} + $index;
    if ($target < 0) { $target += $self->{capacity}; }
    else { $target %= $self->{capacity}; }
    return $self->{values}[$target];
  }

  # range of $cut_offset: [0, $cut_length]
  # O($self->{capacity})
  sub splice : object[] ($self : self, $cut_offset : int, $cut_length : int, $replace : object[]) {
    if ($cut_offset > $self->{length}) {
      warn("splice_with_list() offset past end of array");
      $cut_offset = $self->{length};
      $cut_length = 0;
    }

    # fit cut_length to the end.
    if ($cut_length > $self->{length}) {
      $cut_length = $self->{length};
    }
    if ($cut_offset > $self->{length} - $cut_length) { # always $self->{length} - $cut_length >= 0
      $cut_length = $self->{length} - $cut_offset;
    }

    my $replace_length = 0;
    if ($replace) {
      $replace_length = @$replace;
    }

    if ($self->{length} - $cut_length + $replace_length > $self->{capacity}) {
      # O($new_values_length)
      $self->_reallocate_with_capacity($self->{length} - $cut_length + $replace_length);
    }

    # extract elements
    # O($cut_length)
    my $extracted = new object [$cut_length];
    for (my $i = 0; $i < $cut_length; ++$i) {
      my $target = $self->{offset} + $i;
      if ($target > 0) { $target -= $self->{capacity}; } # to avoid overflow on the next statement
      $target += $cut_offset;
      if ($target < 0) { $target += $self->{capacity}; }
      $extracted->[$i] = $self->{values}[$target];
    }

    my $last_sequence_length = $self->{length} - $cut_offset - $cut_length;
    # move last sequence to forward
    if ($cut_length > $replace_length) {
      for (my $i = 0; $i < $last_sequence_length; ++$i) {
        my $origin = $self->{offset} + $cut_offset + $cut_length + $i;
        if ($origin < 0) { $origin += $self->{capacity}; }
        if ($origin >= $self->{capacity}) { $origin -= $self->{capacity}; }
        my $target = $self->{offset} + $cut_offset + $replace_length + $i;
        if ($target < 0) { $target += $self->{capacity}; }
        if ($target >= $self->{capacity}) { $target -= $self->{capacity}; }
        $self->{values}[$target] = $self->{values}[$origin];
      }
      # deallocate unused sequence
      for (my $i = 0; $i < $cut_length - $replace_length; ++$i) {
        my $target = $self->{offset} + $cut_offset + $replace_length + $last_sequence_length + $i;
        if ($target < 0) { $target += $self->{capacity}; }
        if ($target >= $self->{capacity}) { $target -= $self->{capacity}; }
        $self->{values}[$target] = undef;
      }
    }
    else {
      # move last sequence to backward
      for (my $i = 0; $i < $last_sequence_length; ++$i) {
        my $origin = $self->{offset} + $self->{length} - 1 - $i;
        if ($origin < 0) { $origin += $self->{capacity}; }
        my $target = $self->{offset} + $cut_offset + $replace_length + $last_sequence_length - 1 - $i;
        if ($target < 0) { $target += $self->{capacity}; }
        if ($target >= $self->{capacity}) { $target -= $self->{capacity}; }
        $self->{values}[$target] = $self->{values}[$origin];
      }
    }

    # replace with new array
    for (my $i = 0; $i < $replace_length; ++$i) {
      my $target = $self->{offset} + $cut_offset + $i;
      if ($target < 0) {
        $target += $self->{capacity};
      }
      $self->{values}[$target] = $replace->[$i];
    }

    # update fields
    $self->{length} += $replace_length - $cut_length;

    return $extracted;
  }
}
