class List {
  use Fn;
  use Array;
  
  # Fields
  has values : ro object[];
  has length : ro int;
  
  # Private fields
  has capacity : int;
  
  # Class methods
  static method new : List ($objects : object[]...) {
    my $self = new List;
    
    my $length = 0;
    if ($objects) {
      $length = @$objects;
    }
    
    my $capacity = $length;
    $self->{capacity} = $capacity;
    
    if ($objects) {
      $self->{values} = Array->new_proto($objects, $capacity);
    }
    else {
      $self->{values} = Array->new_proto(new object[0], $capacity);
    }
    
    if ($objects) {
      Array->memcpy_object_address($self->{values}, 0, $objects, 0, $length);
    }
    
    $self->{length} = $length;
    
    return $self;
  }
  
  static method new_len : List ($proto_array : object[], $length : int) {
    unless ($proto_array) {
      die "The prototype array must be defined";
    }
    
    my $self = new List;
    unless ($length >= 0) {
      die "The length must be greater than or equal to 0";
    }
    my $capacity = $length;
    $self->{capacity} = $capacity;
    $self->{length} = $length;
    $self->{values} = Array->new_proto($proto_array, $capacity);
    return $self;
  }
  
  # Instance methods
  method get : object ($index : int) {
    my $length = $self->length;
    
    unless ($index >= 0) {
      die "The index must be greater than or equal to 0";
    }
    
    unless ($index < $length) {
      die "The index must be less than the length of the list";
    }
    
    my $value = $self->{values}[$index];
    
    return $value;
  }
  
  method insert : void ($index : int, $value : object) {
    my $length = $self->{length};
    my $capacity = $self->{capacity};
    
    unless ($index >= 0) {
      die "The index must be greater than or equal to 0";
    }
    
    unless ($index <= $length) {
      die "The index must be less than or equal to the length of the list";
    }
    
    my $new_length = $length + 1;
    $self->_maybe_extend($new_length);
    
    my $values = $self->{values};
    if ($index != $length) {
      Array->memmove_object_address($values, $index + 1, $values, $index, $length - $index);
    }
    $values->[$index] = $value;
    
    $self->{length}++;
  }
  
  method pop : object () {
    my $length = $self->length;
    
    unless ($length > 0) {
      die "The length of the list must be greater than 0";
    }
    
    my $index = $self->{length};
    
    my $ret = $self->{values}[$index - 1];
    
    $self->{values}[$index - 1] = undef;
    
    --$self->{length};
    
    return $ret;
  }
  
  method push : void ($value : object) {
    my $length = $self->{length};
    my $capacity = $self->{capacity};
    
    my $new_length = $length + 1;
    $self->_maybe_extend($new_length);
    my $index = $self->{length};
    $self->{values}[$index] = $value;
    ++$self->{length};
  }
  
  method remove : object ($index : int) {
    my $length = $self->{length};
    my $capacity = $self->{capacity};
    
    unless ($index >= 0) {
      die "The index must be greater than or equal to 0";
    }
    
    unless ($index < $length) {
      die "The index must be less than the length of the list";
    }
    
    my $values = $self->{values};
    my $remove_value = $values->[$index];
    
    my $move_length = $length - $index - 1;
    Array->memmove_object_address($values, $index, $values, $index + 1, $move_length);
    $values->[$length - 1] = undef;
    
    $self->{length}--;
    
    return $remove_value;
  }
  
  method replace : void ($offset : int, $remove_length : int, $replace : object[]) {
    unless ($offset >= 0) {
      die("The offset must be greater than or equal to 0");
    }
    
    unless ($remove_length >= 0) {
      die("The removing length must be greater than or equal to 0");
    }
    unless ($offset + $remove_length <= $self->{length}) {
      die("The offset + the removing lenght must be less than or equal to the length of the list");
    }
    
    my $replace_length = 0;
    if ($replace) {
      $replace_length = @$replace;
    }
    
    my $new_length = $self->{length} - $remove_length + $replace_length;
    $self->_maybe_extend($new_length);
    
    my $move_length = $self->{length} - $offset - $remove_length;
    Array->memmove_object_address($self->{values}, $offset + $replace_length, $self->{values}, $offset + $remove_length, $move_length);
    
    if ($replace) {
      Array->memcpy_object_address($self->{values}, $offset, $replace, 0, $replace_length);
    }
    
    $self->{length} = $new_length;
  }
  
  method resize : void ($new_length : int) {
    unless ($new_length >= 0) {
      die "The new length must be greater than or equal to 0";
    }
    
    my $length = $self->{length};
    my $capacity = $self->{capacity};
    
    if ($new_length > $length) {
      $self->_maybe_extend($new_length);
    }
    elsif ($new_length < $length) {
      Array->memset_object($self->{values}, $new_length, undef, $length - $new_length);
    }
    $self->{length} = $new_length;
  }
  
  method set : void ($index : int, $value : object) {
    my $length = $self->length;
    
    unless ($index >= 0) {
      die "The index must be greater than or equal to 0";
    }
    
    unless ($index < $length) {
      die "The index must be less than the length of the list";
    }
    
    $self->{values}[$index] = $value;
  }
  
  method set_array : void ($array : object[]) {
    unless ($array) {
      die "The array must be defined";
    }
    
    my $cur_length = $self->length;
    
    my $set_length = @$array;
    
    unless ($set_length == $cur_length) {
      die "The length of the array must be the same as the length of the list";
    }
    
    my $values = $self->{values};
    Array->memcpy_object_address($values, 0, $array, 0, $cur_length);
  }
  
  method shift : object () {
    my $length = $self->{length};
    my $capacity = $self->{capacity};
    
    unless ($length > 0) {
      die "The length of the list must be greater than 0";
    }
    
    my $values = $self->{values};
    
    my $value = $values->[0];
    
    Array->memmove_object_address($values, 0, $values, 1, $length - 1);
    $values->[$length - 1] = undef;
    
    $self->{length}--;
    
    return $value;
  }
  
  method to_array : object[] () {
    my $length = $self->{length};
    my $objects = Array->new_proto($self->{values}, $length);
    Array->memcpy_object_address($objects, 0, $self->{values}, 0, $length);
    return $objects;
  }
  
  method unshift : void ($value : object) {
    my $length = $self->{length};
    my $capacity = $self->{capacity};
    
    my $new_length = $length + 1;
    $self->_maybe_extend($new_length);
    
    my $values = $self->{values};
    
    Array->memmove_object_address($values, 1, $values, 0, $length);
    
    $values->[0] = $value;
    $self->{length}++;
  }
  
  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_values = Array->new_proto($self->{values}, $new_capacity);
    
    my $length = $self->{length};
    my $values = $self->{values};
    Array->memcpy_object_address($new_values, 0, $values, 0, $length);
    
    $self->{values} = $new_values;
    $self->{capacity} = $new_capacity;
  }
}