# Copyright (c) 2023 Yuki Kimoto
# MIT License

class Hash {
  interface Cloneable;
  
  use Hash::Entry;
  use Fn;
  use Array;
  use Time;
  use Sort;

  has bucket_count : int;
  has count : ro int;
  has entries : Hash::Entry[];
  has seed128 : private mutable string;

  private enum {
    _DEFALUT_HASH_TABLE_LENGTH = 16,
  }
  private static method _MAX_LOAD_FACTOR : double () { return 0.75; }

  precompile static method new : Hash ($key_values = undef : object[]) {
    my $self = new Hash;
    
    my $default_capacity = 32;
    $self->{bucket_count} = Hash->_DEFALUT_HASH_TABLE_LENGTH();
    $self->{entries} = new Hash::Entry[Hash->_DEFALUT_HASH_TABLE_LENGTH()];
    $self->{count} = 0;

    my $seed_int32 = (int)(Time->time * class_id Hash);
    $self->{seed128} = (mutable string)new_string_len 16;
    $self->build_seed128($seed_int32, $self->{seed128});
    
    my $length : int;
    if ($key_values) {
      $length = @$key_values;
    }
    else {
      $length = 0;
    }
    
    if ($length % 2 != 0) {
      die "The length of the elements in the \$key_values must be an even number";
    }
    
    if ($key_values) {
      for (my $i = 0; $i < @$key_values; $i += 2) {
        my $key = (string)$key_values->[$i];
        my $value = $key_values->[$i + 1];
        unless ($key isa string) {
          die "The key in the \$key_values must be string";
        }

        $self->set($key => $value);
      }
    }
    
    return $self;
  }

  native private method build_seed128 : void ($seed_int32 : int, $seed128 : mutable string);

  method set : void ($key : string, $val : object) {
    unless ($key) {
      die "The \$key must be defined";
    }
    # Rehash
    if (((double)$self->{count} / $self->{bucket_count}) > Hash->_MAX_LOAD_FACTOR()) {
      $self->_rehash;
    }
    my $count = $self->{count};
    $self->_set_to_container($self->{entries}, $self->{bucket_count}, \$count, $key, $val);
    
    $self->{count} = $count;
  }

  precompile method get : object ($key : string) {
    my $index = $self->_index_by_key($key, $self->{bucket_count});
    my $ref = $self->{entries}->[$index];
    unless ($ref) {
      return undef;
    }
    while (1) {
      if ($ref->{key} eq $key) {
        return $ref->{val};
      }
      unless ($ref->{next_entry}) {
        return undef;
      }
      $ref = $ref->{next_entry};
    }
  }

  precompile method exists : int ($key : string) {
    my $index = $self->_index_by_key($key, $self->{bucket_count});
    my $ref = $self->{entries}->[$index];

    unless ($ref) {
      return 0;
    }
    while (1) {
      if ($ref->{key} eq $key) {
        return 1;
      }
      unless ($ref->{next_entry}) {
        return 0;
      }
      $ref = $ref->{next_entry};
    }
  }

  precompile method delete : object ($key : string) {
    my $index = $self->_index_by_key($key, $self->{bucket_count});
    my $ref = $self->{entries}->[$index];
    my $prev : Hash::Entry = undef;
    unless ($ref) {
      return undef;
    }
    while (1) {
      if ($ref->{key} eq $key) {
        my $ret = $ref->{val};
        if ($prev) {
          $prev->{next_entry} = $ref->{next_entry};
        }
        else {
          $self->{entries}->[$index] = $ref->{next_entry};
        }
        $self->{count}--;
        return $ret;
      }
      unless ($ref->{next_entry}) {
        return undef;
      }
      $prev = $ref;
      $ref = $ref->{next_entry};
    }
  }

  precompile method keys : string[] () {
    my $keys = new string[$self->{count}];
    # iterate entries
    my $count = 0;
    for (my $i = 0; $i < $self->{bucket_count}; ++$i) {
      my $ref = $self->{entries}->[$i];
      while ($ref) {
        $keys->[$count++] = $ref->{key};
        $ref = $ref->{next_entry};
      }
    }
    return $keys;
  }

  precompile method values : object[] () {
    my $retval = new object[$self->{count}];
    # iterate entries
    my $count = 0;
    for (my $i = 0; $i < $self->{bucket_count}; ++$i) {
      my $ref = $self->{entries}->[$i];
      while ($ref) {
        $retval->[$count++] = $ref->{val};
        $ref = $ref->{next_entry};
      }
    }
    return $retval;
  }

  precompile method copy : Hash () {
    my $ret = Hash->new({});
    my $keys = $self->keys;
    for (my $i = 0; $i < @$keys; ++$i) {
      $ret->set($keys->[$i], $self->get($keys->[$i]));
    }
    return $ret;
  }

  precompile method to_array : object[] ($sort = 0 : int) {
    my $keys = $self->keys;
    my $keys_length = @$keys;
    
    if ($sort) {
      Sort->sort_string_asc($keys);
    }
    
    my $array = new object[$keys_length * 2];
    for (my $i = 0; $i < $keys_length; $i++) {
      my $key = $keys->[$i];
      $array->[$i * 2] = $key;
      my $value = $self->get($key);
      $array->[$i * 2 + 1] = $value;
    }
    
    return $array;
  }

  method clone : Hash () {
    return $self->copy;
  }

  native static method _murmur_hash : long ($string : string, $seed : int);

  private native static method _siphash13 : long ($string : string, $seed : string);

  method set_byte : void ($key : string, $value : int) {
    $self->set($key => Byte->new($value));
  }
  method set_short : void ($key : string, $value : int) {
    $self->set($key => Short->new($value));
  }
  method set_int : void ($key : string, $value : int) {
    $self->set($key => Int->new($value));
  }
  method set_string : void ($key : string, $value : string) {
    $self->set($key => $value);
  }
  method set_long : void ($key : string, $value : long) {
    $self->set($key => Long->new($value));
  }
  method set_float : void ($key : string, $value : float) {
    $self->set($key => Float->new($value));
  }
  method set_double : void ($key : string, $value : double) {
    $self->set($key => Double->new($value));
  }

  method get_byte : int ($key : string) {
    return (byte)$self->get($key);
  }
  method get_short : int ($key : string) {
    return (short)$self->get($key);
  }
  method get_int : int ($key : string) {
    return (int)$self->get($key);
  }
  method get_string : string ($key : string) {
    return (string)$self->get($key);
  }
  method get_long : long ($key : string) {
    return (long)$self->get($key);
  }
  method get_float : float ($key : string) {
    return (float)$self->get($key);
  }
  method get_double : double ($key : string) {
    return (double)$self->get($key);
  }

  method _bucket_count : int () {
    return $self->{bucket_count};
  }

  method _entries : Hash::Entry [] () {
    return $self->{entries};
  }

  method _index_by_key : int ($key : string, $bucket_count : int) {
    my $default_seed = $self->{seed128};
    
    my $hash = Hash->_siphash13($key, $default_seed);
    my $index_by_key = (int)($hash remul (long)$bucket_count);
    
    return $index_by_key;
  }

  precompile method _set_to_container : void (
    $entries : Hash::Entry[],
    $bucket_count : int,
    $count_ref : int*,
    $key : string,
    $val : object)
  {
    my $index = $self->_index_by_key($key, $bucket_count);
    
    my $ref = $entries->[$index];
    unless ($ref) {
      $entries->[$index] = new Hash::Entry;
      my $new_key : string;
      if (is_read_only $key) {
        $new_key = $key;
      }
      else {
        $new_key = Fn->copy_string($key);
        make_read_only $new_key;
      }
      
      $entries->[$index]->{key} = $new_key;
      $entries->[$index]->{val} = $val;
      $entries->[$index]->{next_entry} = undef;
      ++$$count_ref;
      return;
    }
    while (1) {
      if ($ref->{key} eq $key) {
        $ref->{val} = $val;
        return;
      }
      unless ($ref->{next_entry}) {
        my $new_key : string;
        if (is_read_only $key) {
          $new_key = $key;
        }
        else {
          $new_key = Fn->copy_string($key);
          make_read_only $new_key;
        }

        $ref->{next_entry} = new Hash::Entry;
        $ref->{next_entry}->{key} = $new_key;
        $ref->{next_entry}->{val} = $val;
        $ref->{next_entry}->{next_entry} = undef;
        ++$$count_ref;
        return;
      }
      $ref = $ref->{next_entry};
    }
  }

  precompile method _rehash : void () {
    my $new_bucket_count : int;
    $new_bucket_count = $self->{bucket_count} * 2;
    my $new_entries = new Hash::Entry [$new_bucket_count];
    my $new_count = 0;
    # iterate entries
    for (my $i = 0; $i < $self->{bucket_count}; ++$i) {
      my $ref = $self->{entries}->[$i];
      while ($ref) {
        $self->_set_to_container($new_entries, $new_bucket_count, \$new_count, $ref->{key}, $ref->{val});
        $ref = $ref->{next_entry};
      }
    }
    $self->{bucket_count} = $new_bucket_count;
    $self->{entries} = $new_entries;
  }

  method delete_or_default_byte : int ($key : string, $default : int) {
    
    unless ($key) {
      die "The \$key must be defined";
    }
    
    my $value = 0;
    if ($self->exists($key)) {
      $value = (byte)$self->get_byte($key);
      $self->delete($key);
    }
    else {
      $value = (byte)$default;
    }
    
    return $value;
  }

  method delete_or_default_short : int ($key : string, $default : int) {
    
    unless ($key) {
      die "The \$key must be defined";
    }
    
    my $value = 0;
    if ($self->exists($key)) {
      $value = (short)$self->get_short($key);
      $self->delete($key);
    }
    else {
      $value = (short)$default;
    }
    
    return $value;
  }

  method delete_or_default_int : int ($key : string, $default : int) {
    
    unless ($key) {
      die "The \$key must be defined";
    }
    
    my $value = 0;
    if ($self->exists($key)) {
      $value = $self->get_int($key);
      $self->delete($key);
    }
    else {
      $value = $default;
    }
    
    return $value;
  }

  method delete_or_default_long : long ($key : string, $default : long) {
    
    unless ($key) {
      die "The \$key must be defined";
    }
    
    my $value = 0L;
    if ($self->exists($key)) {
      $value = (long)$self->get_long($key);
      $self->delete($key);
    }
    else {
      $value = $default;
    }
    
    return $value;
  }

  method delete_or_default_float : float ($key : string, $default : float) {
    
    unless ($key) {
      die "The \$key must be defined";
    }
    
    my $value = 0f;
    if ($self->exists($key)) {
      $value = $self->get_float($key);
      $self->delete($key);
    }
    else {
      $value = $default;
    }
    
    return $value;
  }

  method delete_or_default_double : double ($key : string, $default : double) {
    
    unless ($key) {
      die "The \$key must be defined";
    }
    
    my $value = 0.0;
    if ($self->exists($key)) {
      $value = $self->get_double($key);
      $self->delete($key);
    }
    else {
      $value = $default;
    }
    
    return $value;
  }

  method delete_or_default_string : string ($key : string, $default : string) {
    
    unless ($key) {
      die "The \$key must be defined";
    }
    
    my $value = (string)undef;
    if ($self->exists($key)) {
      $value = $self->get_string($key);
      $self->delete($key);
    }
    else {
      $value = $default;
    }
    
    return $value;
  }

  method delete_or_default : object ($key : string, $default : object) {
    
    unless ($key) {
      die "The \$key must be defined";
    }
    
    my $value = (object)undef;
    if ($self->exists($key)) {
      $value = $self->get($key);
      $self->delete($key);
    }
    else {
      $value = $default;
    }
    
    return $value;
  }
}