/**
 *      This class implements an associative array for storing arbitrary types
 *  of data that provides a quick (near constant) access to *values* by
 *  associated *keys*.
 *      Since UnrealScript lacks any sort of templating, `HashTable`
 *  stores generic `AcediaObject` keys and values. `Text` can be used instead of
 *  typical `string` keys and primitive values can be added in their boxed form
 *  (either as actual `<Type>Box` or as it's reference counterpart).
 *      Copyright 2022 Anton Tarasenko
 *------------------------------------------------------------------------------
 * This file is part of Acedia.
 *
 * Acedia is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3 of the License, or
 * (at your option) any later version.
 *
 * Acedia is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Acedia.  If not, see <https://www.gnu.org/licenses/>.
 */
class HashTable extends Collection;

// Defines key <-> value mapping
struct Entry
{
    var AcediaObject key;
    var AcediaObject value;
};

//  Bucket of entries.
//  Used to store entries with the same index in hash table.
struct Bucket
{
    var array<Entry> entries;
};
var private array<Bucket> hashTable;

//      Amount of elements currently stored in this `HashTable`.
//      If one of the keys was deallocated outside of `HashTable`,
//  this value may overestimate actual amount of elements.
var private int storedElementCount;
//  Lower limit on hash table capacity, can be changed by the user.
var private int minimalCapacity;

//  hard lower and upper limits on hash table size, constant.
var private const int MINIMUM_SIZE;
var private const int MAXIMUM_SIZE;
//      Minimum and maximum allowed density of elements
//  (`storedElementCount / hashTable.length`).
//      If density falls outside this range, - we have to resize hash table to
//  get into (MINIMUM_DENSITY; MAXIMUM_DENSITY) bounds, as long as it does not
//  violate hard size restrictions.
//      Actual size changes in multipliers of 2, so
//  `MINIMUM_DENSITY * 2 < MAXIMUM_DENSITY` must hold or we will constantly
//  oscillate outside of (MINIMUM_DENSITY; MAXIMUM_DENSITY) bounds.
var private const float MINIMUM_DENSITY;
var private const float MAXIMUM_DENSITY;

/**
 *  Auxiliary struct, necessary to implement iterator for `HashTable`.
 *  Can be used for manual iteration, but should be avoided in favor of
 *  `Iterator`.
 */
struct Index
{
    var protected int bucketIndex;
    var protected int entryIndex;
};

protected function Constructor()
{
    UpdateHashTableSize();
}

protected function Finalizer()
{
    Empty();
}

//      Auxiliary method that is needed as a replacement for `%` module
//  operator, since it is an operation on `float`s in UnrealScript and does not
//  have appropriate value range to work with hashes.
//      Assumes non-negative input.
private function int Remainder(int number, int divisor)
{
    local int quotient;

    quotient = number / divisor;
    return (number - quotient * divisor);
}

//  Calculates appropriate bucket index for the given key.
//  Assumes that given key is not `none` and is allocated.
private final function int GetBucketIndex(AcediaObject key)
{
    local int bucketIndex;

    bucketIndex = key.GetHashCode();
    if (bucketIndex < 0) {
        //  Minimum `int` value is greater than maximum one in absolute value,
        //  so shift it up to avoid overflow.
        bucketIndex = -1 * (bucketIndex + 1);
    }
    bucketIndex = Remainder(bucketIndex, hashTable.length);
    return bucketIndex;
}

//  Accessing value in `HashTable` requires two level lookup of both bucket and
//  entry (inside that bucket) indices.
//
//      As a result returns bucket's and entry's indices in `bucketIndex` and
//  `entryIndex` inside `out` variables.
//      `bucketIndex` is guaranteed to be found for non-`none` keys (in case you
//  want to add a new entry), `entryIndex` is valid iff method returns `true`,
//  otherwise it's equal to the index at which new property can get inserted.
private final function bool FindEntryIndices(
    AcediaObject    key,
    out int         bucketIndex,
    out int         entryIndex)
{
    local int           i;
    local array<Entry>  bucketEntries;

    if (key == none){
        return false; 
    }
    bucketIndex = GetBucketIndex(key);
    //  Check if bucket actually has given key.
    bucketEntries = hashTable[bucketIndex].entries;
    for (i = 0; i < bucketEntries.length; i += 1)
    {
        if (key.IsEqual(bucketEntries[i].key))
        {
            entryIndex = i;
            return true;
        }
    }
    entryIndex = bucketEntries.length;
    return false;
}

//  Checks if we need to change our current hash table size
//  and does so if needed
private final function UpdateHashTableSize()
{
    local int oldSize, newSize;

    oldSize = hashTable.length;
    //  Calculate new size (and whether it is needed) based on amount of
    //  stored properties and current size
    newSize = oldSize;
    if (storedElementCount < newSize * MINIMUM_DENSITY) {
        newSize /= 2;
    }
    else if (storedElementCount > newSize * MAXIMUM_DENSITY) {
        newSize *= 2;
    }
    //  `table_density = items_amount / table_size`, so to store at least
    //  `items_amount = minimalCapacity` without making table too dense we need
    //  `table_size = minimalCapacity / MAXIMUM_DENSITY`.
    newSize = Max(newSize, Ceil(minimalCapacity / MAXIMUM_DENSITY));
    //  But everything must fall into the set hard limits
    newSize = Clamp(newSize, MINIMUM_SIZE, MAXIMUM_SIZE);
    //  Only resize if difference is huge enough or table does not exists yet
    if (newSize != oldSize) {
        ResizeHashTable(newSize);
    }
}

//      Changes size of the hash table, does not check any limits,
//  does not check if `newSize` is a valid size (`newSize > 0`).
private final function ResizeHashTable(int newSize)
{
    local int           i, j;
    local int           newBucketIndex, newEntryIndex;
    local array<Entry>  bucketEntries;
    local array<Bucket> oldHashTable;

    oldHashTable = hashTable;
    //  Clean current hash table
    hashTable.length = 0;
    hashTable.length = newSize;
    for (i = 0; i < oldHashTable.length; i += 1)
    {
        bucketEntries = oldHashTable[i].entries;
        for (j = 0; j < bucketEntries.length; j += 1)
        {
            newBucketIndex  = GetBucketIndex(bucketEntries[j].key);
            newEntryIndex   = hashTable[newBucketIndex].entries.length;
            hashTable[newBucketIndex].entries[newEntryIndex] = bucketEntries[j];
        }
    }
}

/**
 *  Appends objects from another `HashTable` to the caller one.
 *
 *  @param  other   Array to append objects from. `none` means nothing will be
 *      added.
 *  @return Reference to the caller `HashTable` to allow for method chaining.
 */
public final function HashTable Append(HashTable other)
{
    local AcediaObject      nextKey, nextValue;
    local HashTableIterator iter;

    if (other == none)          return self;
    if (other.GetLength() <= 0) return self;

    iter = HashTableIterator(other.Iterate());
    while (!iter.HasFinished())
    {
        nextKey     = iter.GetKey();
        nextValue   = iter.Get();
        if (!HasKey(nextKey)) {
            SetItem(nextKey, nextValue);
        }
        _.memory.Free(nextKey);
        _.memory.Free(nextValue);
        iter.Next();
    }
    _.memory.Free(iter);
    return self;
}

/**
 *  Returns minimal capacity of the caller associative array.
 *
 *  See `SetMinimalCapacity()` for details.
 *
 *  @return Minimal capacity of the caller associative array. Default is zero.
 */
public final function int GetMinimalCapacity()
{
    return minimalCapacity;
}

/**
 *  Returns minimal capacity of the caller associative array.
 *
 *      This associative array works like a hash table and needs to allocate
 *  sufficiently large dynamic array as a storage for its items.
 *  If you keep adding new items that storage will eventually become too small
 *  for hash table to work efficiently and we will have to reallocate and
 *  re-fill it. If you want to add a huge enough amount of items, this process
 *  can be repeated several times.
 *      This is not ideal, since it means doing a lot of iteration, each
 *  increasing infinite loop counter (game will crash if it gets high enough).
 *      Setting minimal capacity to the (higher) amount of items you expect to
 *  store in the caller array can remove the need for reallocating the storage.
 *
 *  @param  newMinimalCapacity  New minimal capacity of this associative array.
 *      It's recommended to set it to the max amount of items you expect to
 *      store in this associative array
 *      (you will be still allowed to store more).
 */
public final function SetMinimalCapacity(int newMinimalCapacity)
{
    minimalCapacity = newMinimalCapacity;
    UpdateHashTableSize();
}

public function bool HasKey(AcediaObject key)
{
    local int bucketIndex, entryIndex;

    return FindEntryIndices(key, bucketIndex, entryIndex);
}

public function bool HasKeyByText(Text key)
{
    return HasKey(key);
}

/**
 *  Returns borrowed value recorded by a given key `key` in the caller
 *  `HashTable`.
 *
 *  Can return `none` if either stored values is `none` or there's no value
 *  recorded with a `key`. To check whether there is a record, corresponding to
 *  the `key` use `HasKey()` method.
 *
 *  @param  key Key for which to return value.
 *  @return Value, stored with given key `key`. If there is no value with
 *      such a key method will return `none`.
 */
private final function AcediaObject BorrowItem(AcediaObject key)
{
    local int bucketIndex, entryIndex;

    if (FindEntryIndices(key, bucketIndex, entryIndex)) {
        return hashTable[bucketIndex].entries[entryIndex].value;
    }
    return none;
}

/**
 *  Returns value recorded by a given key `key` in the caller
 *  `HashTable`.
 *
 *  Can return `none` if either stored values is `none` or there's no value
 *  recorded with a `key`. To check whether there is a record, corresponding to
 *  the `key` use `HasKey()` method.
 *
 *  @param  key Key for which to return value.
 *  @return Value, stored with given key `key`. If there is no value with
 *      such a key method will return `none`.
 */
public final function AcediaObject GetItem(AcediaObject key)
{
    local int           bucketIndex, entryIndex;
    local AcediaObject  result;

    if (FindEntryIndices(key, bucketIndex, entryIndex)) {
        result = hashTable[bucketIndex].entries[entryIndex].value;
    }
    if (result != none) {
        return result.NewRef();
    }
    return none;
}

/**
 *  Returns entry corresponding to a given key `key` in the caller
 *  `HashTable`.
 *
 *  @param  key Key for which to return entry.
 *  @return Entry (key/value pair) with the given key `key`.
 */
public final function Entry GetEntry(AcediaObject key)
{
    local Entry result;
    local int   bucketIndex, entryIndex;

    if (!FindEntryIndices(key, bucketIndex, entryIndex)) {
        return result;
    }
    result = hashTable[bucketIndex].entries[entryIndex];
    if (result.key != none) {
        result.key.NewRef();
    }
    if (result.value != none) {
        result.value.NewRef();
    }
    return result;
}

/**
 *  Returns entry corresponding to a given key `key` in the caller
 *  `HashTable`, removing it from the caller `HashTable`.
 *
 *  @param  key Key for which to return entry.
 *  @return Entry (key/value pair) with the given key `key`.
 */
public final function Entry TakeEntry(AcediaObject key)
{
    local Entry entryToTake;
    local int   bucketIndex, entryIndex;

    if (!FindEntryIndices(key, bucketIndex, entryIndex)) {
        return entryToTake;
    }
    entryToTake = hashTable[bucketIndex].entries[entryIndex];
    hashTable[bucketIndex].entries.Remove(entryIndex, 1);
    storedElementCount = Max(0, storedElementCount - 1);
    UpdateHashTableSize();
    return entryToTake;
}

/**
 *  Returns value recorded with a given key `key` in the caller
 *  `HashTable`, removing it from the collection.
 *
 *  @param  key     Key for which to return value.
 *  @param  freeKey Setting this to `true` will also free the key item was
 *      stored with. Passed argument `key` will not be deallocated, unless it is
 *      the exact same object as item's key inside caller collection.
 *  @return Value, stored with given key `key`. If there is no value with
 *      such a key method will return `none`.
 */
public final function AcediaObject TakeItem(AcediaObject key)
{
    local Entry entry;

    entry = TakeEntry(key);
    if (entry.key != none) {
        entry.key.FreeSelf();
    }
    return entry.value;
}

/**
 *  Records new `value` under the key `key` into the caller `HashTable`.
 *
 *  @param  key     Key by which new value will be referred to.
 *  @param  value   Value to store in the caller `HashTable`.
 *  @return Caller `HashTable` to allow for method chaining.
 */
public final function HashTable SetItem(
    AcediaObject    key,
    AcediaObject    value)
{
    local Entry oldEntry, newEntry;
    local int   bucketIndex, entryIndex;

    if (key == none) {
        return self; 
    }
    if (FindEntryIndices(key, bucketIndex, entryIndex)) {
        oldEntry = hashTable[bucketIndex].entries[entryIndex];
    }
    else {
        storedElementCount += 1;
    }
    key.NewRef();
    _.memory.Free(oldEntry.key);
    newEntry.key    = key;
    newEntry.value  = value;
    if (value != none) {
        value.NewRef();
    }
    if (oldEntry.value != none) {
        oldEntry.value.FreeSelf();
    }
    hashTable[bucketIndex].entries[entryIndex] = newEntry;
    return self;
}

/**
 *  Creates a new instance of class `valueClass` and records it's value with
 *  key `key` in the caller `HashTable`.
 *
 *  @param  key         Key by which new value will be referred to.
 *  @param  valueClass  Class of object to create. Will only be created if
 *      passed `key` is valid.
 *  @return Caller `HashTable` to allow for method chaining.
 */
public final function HashTable CreateItem(
    AcediaObject        key,
    class<AcediaObject> valueClass)
{
    local AcediaObject newItem;

    if (key == none)        return self;
    if (valueClass == none) return self;

    newItem = _.memory.Allocate(valueClass);
    SetItem(key, newItem);
    newItem.FreeSelf();
    return self;
}

/**
 *  Removes a value recorded with a given key `key`.
 *  Does nothing if entry with a given key does not exist.
 *
 *  @param  key Key for which to remove value.
 *  @return Caller `HashTable` to allow for method chaining.
 */
public final function HashTable RemoveItem(AcediaObject key)
{
    local Entry entryToRemove;
    local int   bucketIndex, entryIndex;

    if (key == none)                                        return self;
    if (!FindEntryIndices(key, bucketIndex, entryIndex))    return self;

    entryToRemove = hashTable[bucketIndex].entries[entryIndex];
    hashTable[bucketIndex].entries.Remove(entryIndex, 1);
    storedElementCount = Max(0, storedElementCount - 1);
    UpdateHashTableSize();
    if (entryToRemove.value != none) {
        entryToRemove.value.FreeSelf();
    }
    if (entryToRemove.key != none) {
        entryToRemove.key.FreeSelf();
    }
    return self;
}

public function Empty()
{
    local int           i, j;
    local array<Entry>  nextEntries;

    for (i = 0; i < hashTable.length; i += 1)
    {
        nextEntries = hashTable[i].entries;
        for (j = 0; j < nextEntries.length; j += 1)
        {
            if (nextEntries[j].value != none) {
                nextEntries[j].value.FreeSelf();
            }
            if (nextEntries[j].key != none) {
                nextEntries[j].key.FreeSelf();
            }
        }
    }
    hashTable.length = 0;
    storedElementCount = 0;
    UpdateHashTableSize();
}

/**
 *  Returns key of all properties inside caller `HashTable`.
 *
 *  Collecting all keys from the `HashTable` is O(<number_of_elements>).
 *
 *  See also `GetTextKeys()` methods.
 *
 *  @return Array of all the caller `HashTable`'s keys.
 *      This method does not return copies of keys, but actual keys instead -
 *      deallocating them will remove their item from
 *      the caller `HashTable`.
 */
public final function array<AcediaObject> GetKeys()
{
    local int                   i, j;
    local array<AcediaObject>   result;
    local array<Entry>          nextEntry;

    for (i = 0; i < hashTable.length; i += 1)
    {
        nextEntry = hashTable[i].entries;
        for (j = 0; j < nextEntry.length; j += 1)
        {
            nextEntry[j].key.NewRef();
            result[result.length] = nextEntry[j].key;
        }
    }
    return result;
}

/**
 *  Returns copies of `Text` key of all properties inside caller
 *  `HashTable`. Keys that have a different class (even if they are
 *  a child class for `Text`) are not returned.
 *
 *  Collecting all keys from the `HashTable` is O(<number_of_elements>).
 *
 *  @return Array of all the caller `HashTable`'s keys that have exactly
 *      `Text` class.
 */
public final function array<Text> GetTextKeys()
{
    local int           i, j;
    local Text          nextKeyAsText;
    local array<Text>   result;
    local array<Entry>  nextEntry;

    for (i = 0; i < hashTable.length; i += 1)
    {
        nextEntry = hashTable[i].entries;
        for (j = 0; j < nextEntry.length; j += 1)
        {
            nextKeyAsText = Text(nextEntry[j].key);
            if (nextKeyAsText != none && nextKeyAsText.class == class'Text')
            {
                result[result.length] = nextKeyAsText.Copy();
            }
        }
    }
    return result;
}

/**
 *  Returns amount of elements in the caller `HashTable`.
 *
 *      Note that this value might overestimate real amount of values inside
 *  `HashTable` in case some of the keys used for storage were
 *  deallocated by code outside of `HashTable`.
 *      Such values might be eventually found and removed, but
 *  `HashTable` does not provide any guarantees on when it's done.
 */
public final function int GetLength()
{
    return storedElementCount;
}

/**
 *  Auxiliary method for iterator that increments given `Index` structure.
 *
 *  @param  previousIndex   Index to increment.
 *  @return `true` if incremented index is pointing at a valid item,
 *      `false` if collection has ended.
 */
public final function bool IncrementIndex(out Index previousIndex)
{
    previousIndex.entryIndex += 1;
    //  Go forward through buckets until we find non-empty one
    while (previousIndex.bucketIndex < hashTable.length)
    {
        if (    previousIndex.entryIndex
            <   hashTable[previousIndex.bucketIndex].entries.length)
        {
            return true;
        }
        previousIndex.entryIndex = 0;
        previousIndex.bucketIndex += 1;
    }
    return false;
}

/**
 *  Auxiliary method for iterator that returns value corresponding to
 *  a given `Index` structure.
 *
 *  @param  index   Index of item to return.
 *  @return Item corresponding to a given index.
 *      If index is invalid returns `none`.
 *      Note that `none` can be returned because that is simply the value
 *      being stored.
 */
public final function AcediaObject GetItemByIndex(Index index)
{
    local AcediaObject item;

    if (index.bucketIndex < 0)                  return none;
    if (index.bucketIndex >= hashTable.length)  return none;

    if (    index.entryIndex < 0
        ||  index.entryIndex >= hashTable[index.bucketIndex].entries.length) {
        return none;
    }
    item = hashTable[index.bucketIndex].entries[index.entryIndex].value;
    if (item == none) {
        return none;
    }
    return item.NewRef();
}

/**
 *  Auxiliary method for iterator that returns value corresponding to
 *  a given `Index` structure.
 *
 *  @param  index   Index of item to return.
 *  @return Key corresponding to a given index.
 *      If index is invalid returns `none`.
 */
public final function AcediaObject GetKeyByIndex(Index index)
{
    local AcediaObject key;

    if (index.bucketIndex < 0)                  return none;
    if (index.bucketIndex >= hashTable.length)  return none;
    if (    index.entryIndex < 0
        ||  index.entryIndex >= hashTable[index.bucketIndex].entries.length) {
        return none;
    }
    key = hashTable[index.bucketIndex].entries[index.entryIndex].key;
    if (key == none) {
        return none;
    }
    return key.NewRef();
}

public function AcediaObject GetByText(Text key)
{
    return GetItem(key);
}

/**
 *  Checks if value corresponding to a given `Index` structure is not `none`.
 *
 *  @param  key Key to check the value at.
 *  @return `true` if non-`none` value is recorded at `key` and
 *      `false` otherwise.
 */
public final function bool IsSomethingByIndex(Index index)
{
    if (index.bucketIndex < 0)                  return false;
    if (index.bucketIndex >= hashTable.length)  return false;

    if (    index.entryIndex < 0
        ||  index.entryIndex >= hashTable[index.bucketIndex].entries.length) {
        return false;
    }
    return
        (hashTable[index.bucketIndex].entries[index.entryIndex].value != none);
}

/**
 *  Checks if value recorded at a given `key` is not `none`.
 *
 *  @param  key Key to check the value at.
 *  @return `true` if non-`none` value is recorded at `key` and
 *      `false` otherwise.
 */
public final function bool IsSomething(AcediaObject key)
{
    return (BorrowItem(key) != none);
}

/**
 *  Returns `bool` item at key `key`. If key is invalid or
 *  stores a non-`bool` value, returns `defaultValue`.
 *
 *  Referred value must be stored as `BoolBox` or `BoolRef`
 *  (or one of their sub-classes) for this method to work.
 *
 *  @param  key             Key of a `bool` item that `HashTable` has to return.
 *  @param  defaultValue    Value to return if there is either no item recorded
 *      at `key` or it has a wrong type.
 *  @return `bool` value at `key` in the caller `HashTable`.
 *      `defaultValue` if passed `key` is invalid or non-`bool` value
 *      is stored with it.
 */
public final function bool GetBool(AcediaObject key, optional bool defaultValue)
{
    local AcediaObject  result;
    local BoolBox       asBox;
    local BoolRef       asRef;

    result = BorrowItem(key);
    if (result == none) {
        return defaultValue;
    }
    asBox = BoolBox(result);
    if (asBox != none) {
        return asBox.Get();
    }
    asRef = BoolRef(result);
    if (asRef != none) {
        return asRef.Get();
    }
    return defaultValue;
}

/**
 *  Changes `HashTable`'s value at key `key` to `value` that will be
 *  recorded as either `BoolBox` or `BoolRef`, depending of `asRef`
 *  optional parameter.
 *
 *  @param  key     Key, at which to change the value.
 *  @param  value   Value to be set at a given key.
 *  @param  asRef   Given `bool` value will be recorded as immutable `BoolBox`
 *      by default (`asRef == false`). Setting this parameter to `true` will
 *      make this method record it as a mutable `BoolRef`.
 *  @return Reference to the caller `HashTable` to allow for
 *      method chaining.
 */
public final function HashTable SetBool(
    AcediaObject    key,
    bool            value,
    optional bool   asRef)
{
    local AcediaObject newValue;

    if (asRef) {
        newValue = _.ref.bool(value);
    }
    else {
        newValue = _.box.bool(value);
    }
    SetItem(key, newValue);
    newValue.FreeSelf();
    return self;
}

/**
 *  Returns `byte` item at key `key`. If key is invalid or
 *  stores a non-`byte` value, returns `defaultValue`.
 *
 *  Referred value must be stored as `ByteBox` or `ByteBox`
 *  (or one of their sub-classes) for this method to work.
 *
 *  @param  key             Key of a `byte` item that `HashTable`
 *      has to return.
 *  @param  defaultValue    Value to return if there is either no item recorded
 *      at `key` or it has a wrong type.
 *  @return `byte` value at `key` in the caller `HashTable`.
 *      `defaultValue` if passed `key` is invalid or non-`byte` value
 *      is stored with it.
 */
public final function byte GetByte(AcediaObject key, optional byte defaultValue)
{
    local AcediaObject  result;
    local ByteBox       asBox;
    local ByteRef       asRef;

    result = BorrowItem(key);
    if (result == none) {
        return defaultValue;
    }
    asBox = ByteBox(result);
    if (asBox != none) {
        return asBox.Get();
    }
    asRef = ByteRef(result);
    if (asRef != none) {
        return asRef.Get();
    }
    return defaultValue;
}

/**
 *  Changes `HashTable`'s value at key `key` to `value` that will be
 *  recorded as either `ByteBox` or `ByteBox`, depending of `asRef`
 *  optional parameter.
 *
 *  @param  key     Key, at which to change the value.
 *  @param  value   Value to be set at a given key.
 *  @param  asRef   Given `byte` value will be recorded as immutable `ByteBox`
 *      by default (`asRef == false`). Setting this parameter to `true` will
 *      make this method record it as a mutable `ByteBox`.
 *  @return Reference to the caller `HashTable` to allow for
 *      method chaining.
 */
public final function HashTable SetByte(
    AcediaObject    key,
    byte            value,
    optional bool   asRef)
{
    local AcediaObject newValue;

    if (asRef) {
        newValue = _.ref.byte(value);
    }
    else {
        newValue = _.box.byte(value);
    }
    SetItem(key, newValue);
    newValue.FreeSelf();
    return self;
}

/**
 *  Returns `int` or `float` item at key `key` as `int`. If key is invalid or
 *  stores a non-`int` (or non-`float`) value, returns `defaultValue`.
 *
 *  Referred value must be stored as `IntBox`, `IntRef`, `FloatBox` or
 *  `FloatRef` (or one of their sub-classes) for this method to work.
 *
 *  Allowing for implicit conversion between non-`byte` numeric types simplifies
 *  handling parsed input as there is no need to know whether parsed value is
 *  expected to be integer or floating point.
 *
 *  @param  key             Key of a `int` item that `HashTable`
 *      has to return.
 *  @param  defaultValue    Value to return if there is either no item recorded
 *      at `key` or it has a wrong type.
 *  @return `int` value at `key` in the caller `HashTable`.
 *      `defaultValue` if passed `key` is invalid or non-`int` value
 *      is stored with it.
 */
public final function int GetInt(AcediaObject key, optional int defaultValue)
{
    local AcediaObject  result;
    local IntBox        asBox;
    local IntRef        asRef;
    local FloatBox      asFloatBox;
    local FloatRef      asFloatRef;

    result = BorrowItem(key);
    if (result == none) {
        return defaultValue;
    }
    asBox = IntBox(result);
    if (asBox != none) {
        return asBox.Get();
    }
    asRef = IntRef(result);
    if (asRef != none) {
        return asRef.Get();
    }
    asFloatBox = FloatBox(result);
    if (asFloatBox != none) {
        return int(asFloatBox.Get());
    }
    asFloatRef = FloatRef(result);
    if (asFloatRef != none) {
        return int(asFloatRef.Get());
    }
    return defaultValue;
}

/**
 *  Changes `HashTable`'s value at key `key` to `value` that will be
 *  recorded as either `IntBox` or `IntRef`, depending of `asRef`
 *  optional parameter.
 *
 *  @param  key     Key, at which to change the value.
 *  @param  value   Value to be set at a given key.
 *  @param  asRef   Given `int` value will be recorded as immutable `IntBox`
 *      by default (`asRef == false`). Setting this parameter to `true` will
 *      make this method record it as a mutable `IntRef`.
 *  @return Reference to the caller `HashTable` to allow for
 *      method chaining.
 */
public final function HashTable SetInt(
    AcediaObject    key,
    int             value,
    optional bool   asRef)
{
    local AcediaObject newValue;

    if (asRef) {
        newValue = _.ref.int(value);
    }
    else {
        newValue = _.box.int(value);
    }
    SetItem(key, newValue);
    newValue.FreeSelf();
    return self;
}

/**
 *  Returns `int` or `float` item at key `key` as `float`. If key is invalid or
 *  stores a non-`float` (or non-`int`) value, returns `defaultValue`.
 *
 *  Referred value must be stored as `IntBox`, `IntRef`, `FloatBox` or
 *  `FloatRef` (or one of their sub-classes) for this method to work.
 *
 *  Allowing for implicit conversion between non-`byte` numeric types simplifies
 *  handling parsed input as there is no need to know whether parsed value is
 *  expected to be integer or floating point.
 *
 *  @param  key             Key of a `float` item that `HashTable`
 *      has to return.
 *  @param  defaultValue    Value to return if there is either no item recorded
 *      at `key` or it has a wrong type.
 *  @return `float` value at `key` in the caller `HashTable`.
 *      `defaultValue` if passed `key` is invalid or non-`float` value
 *      is stored with it.
 */
public final function float GetFloat(
    AcediaObject    key,
    optional float  defaultValue)
{
    local AcediaObject  result;
    local FloatBox      asBox;
    local FloatRef      asRef;
    local IntBox        asIntBox;
    local IntRef        asIntRef;

    result = BorrowItem(key);
    if (result == none) {
        return defaultValue;
    }
    asBox = FloatBox(result);
    if (asBox != none) {
        return asBox.Get();
    }
    asRef = FloatRef(result);
    if (asRef != none) {
        return asRef.Get();
    }
    asIntBox = IntBox(result);
    if (asIntBox != none) {
        return float(asIntBox.Get());
    }
    asIntRef = IntRef(result);
    if (asIntRef != none) {
        return float(asIntRef.Get());
    }
    return defaultValue;
}

/**
 *  Changes `HashTable`'s value at key `key` to `value` that will be
 *  recorded as either `FloatBox` or `FloatRef`, depending of `asRef`
 *  optional parameter.
 *
 *  @param  key     Key, at which to change the value.
 *  @param  value   Value to be set at a given key.
 *  @param  asRef   Given `float` value will be recorded as immutable `FloatBox`
 *      by default (`asRef == false`). Setting this parameter to `true` will
 *      make this method record it as a mutable `FloatRef`.
 *  @return Reference to the caller `HashTable` to allow for method chaining.
 */
public final function HashTable SetFloat(
    AcediaObject    key,
    float           value,
    optional bool   asRef)
{
    local AcediaObject newValue;

    if (asRef) {
        newValue = _.ref.float(value);
    }
    else {
        newValue = _.box.float(value);
    }
    SetItem(key, newValue);
    newValue.FreeSelf();
    return self;
}

/**
 *  Returns `Vector` item at key `key`. If key is invalid or
 *  stores a non-`Vector` value, returns `defaultValue`.
 *
 *  Referred value must be stored as `VectorBox` or `VectorRef`
 *  (or one of their sub-classes) for this method to work.
 *
 *  @param  key             Key of a `Vector` item that `HashTable`
 *      has to return.
 *  @param  defaultValue    Value to return if there is either no item recorded
 *      at `key` or it has a wrong type.
 *  @return `Vector` value at `key` in the caller `HashTable`.
 *      `defaultValue` if passed `key` is invalid or non-`Vector` value
 *      is stored with it.
 */
public final function Vector GetVector(
    AcediaObject    key,
    optional Vector defaultValue)
{
    local AcediaObject  result;
    local VectorBox      asBox;
    local VectorRef      asRef;

    result = BorrowItem(key);
    if (result == none) {
        return defaultValue;
    }
    asBox = VectorBox(result);
    if (asBox != none) {
        return asBox.Get();
    }
    asRef = VectorRef(result);
    if (asRef != none) {
        return asRef.Get();
    }
    return defaultValue;
}

/**
 *  Changes `HashTable`'s value at key `key` to `value` that will be
 *  recorded as either `VectorBox` or `VectorRef`, depending of `asRef`
 *  optional parameter.
 *
 *  @param  key     Key, at which to change the value.
 *  @param  value   Value to be set at a given key.
 *  @param  asRef   Given `Vector` value will be recorded as immutable
 *      `VectorBox` by default (`asRef == false`). Setting this parameter to
 *      `true` will make this method record it as a mutable `VectorRef`.
 *  @return Reference to the caller `HashTable` to allow for method chaining.
 */
public final function HashTable SetVector(
    AcediaObject    key,
    Vector          value,
    optional bool   asRef)
{
    local AcediaObject newValue;

    if (asRef) {
        newValue = _.ref.Vec(value);
    }
    else {
        newValue = _.box.Vec(value);
    }
    SetItem(key, newValue);
    newValue.FreeSelf();
    return self;
}

/**
 *  Returns plain string item at key `key`. If key is invalid or stores
 *  a non-`BaseText` value, returns `defaultValue`.
 *
 *  Referred value must be stored as `Text` or `MutableText` (or one of their
 *  sub-classes) for this method to work.
 *
 *  @param  key             Key of a `string` item that `HashTable` has to
 *      return.
 *  @param  defaultValue    Value to return if there is either no item recorded
 *      at `key` or it has a wrong type.
 *  @return Plain string value at `key` in the caller `HashTable`.
 *      `defaultValue` if passed `key` is invalid or non-`BaseText` value is
 *      stored with it.
 */
public final function string GetString(
    AcediaObject    key,
    optional string defaultValue)
{
    local AcediaObject  result;
    local BaseText      asText;

    result = BorrowItem(key);
    if (result == none) {
        return defaultValue;
    }
    asText = BaseText(result);
    if (asText != none) {
        return asText.ToString();
    }
    return defaultValue;
}

/**
 *  Changes `HashTable`'s value at key `key` to plain string `value` that will
 *  be recorded as either `Text` or `MutableText`, depending of `asMutable`
 *  optional parameter.
 *
 *  @param  key         Key, at which to change the value.
 *  @param  value       Value to be set at a given key.
 *  @param  asMutable   Given plain string value will be recorded as immutable
 *      `Text` by default (`asMutable == false`). Setting this parameter to
 *      `true` will make this method record it as a mutable `MutableText`.
 *  @return Reference to the caller `HashTable` to allow for
 *      method chaining.
 */
public final function HashTable SetString(
    AcediaObject    key,
    string          value,
    optional bool   asMutable)
{
    local AcediaObject newValue;

    if (asMutable) {
        newValue = _.text.FromStringM(value);
    }
    else {
        newValue = _.text.FromString(value);
    }
    SetItem(key, newValue);
    newValue.FreeSelf();
    return self;
}

/**
 *  Returns formatted string item at key `key`. If key is invalid or stores
 *  a non-`BaseText` value, returns `defaultValue`.
 *
 *  Referred value must be stored as `Text` or `MutableText` (or one of their
 *  sub-classes) for this method to work.
 *
 *  @param  key             Key of a `string` item that `HashTable` has to
 *      return.
 *  @param  defaultValue    Value to return if there is either no item recorded
 *      at `key` or it has a wrong type.
 *  @return Formatted string value at `key` in the caller `HashTable`.
 *      `defaultValue` if passed `key` is invalid or non-`BaseText` value is
 *      stored with it.
 */
public final function string GetFormattedString(
    AcediaObject    key,
    optional string defaultValue)
{
    local AcediaObject  result;
    local BaseText      asText;

    result = BorrowItem(key);
    if (result == none) {
        return defaultValue;
    }
    asText = BaseText(result);
    if (asText != none) {
        return asText.ToFormattedString();
    }
    return defaultValue;
}

/**
 *  Changes `HashTable`'s value at key `key` to formatted string `value` that
 *  will be recorded as either `Text` or `MutableText`, depending of `asMutable`
 *  optional parameter.
 *
 *  @param  key         Key, at which to change the value.
 *  @param  value       Value to be set at a given key.
 *  @param  asMutable   Given formatted string value will be recorded as
 *      immutable `Text` by default (`asMutable == false`). Setting this
 *      parameter to `true` will make this method record it as a mutable
 *      `MutableText`.
 *  @return Reference to the caller `HashTable` to allow for
 *      method chaining.
 */
public final function HashTable SetFormattedString(
    AcediaObject    key,
    string          value,
    optional bool   asMutable)
{
    local AcediaObject newValue;

    if (asMutable) {
        newValue = _.text.FromFormattedStringM(value);
    }
    else {
        newValue = _.text.FromFormattedString(value);
    }
    SetItem(key, newValue);
    newValue.FreeSelf();
    return self;
}

/**
 *  Returns `Text` item stored at key `key`. If key is invalid or
 *  stores a non-`Text` value, returns `none`.
 *
 *  Referred value must be stored as `Text` (or one of it's sub-classes,
 *  such as `MutableText`) for this method to work.
 *
 *  @param  key Key of a `Text` item that `HashTable`
 *      has to return.
 *  @return `Text` value recorded with `key` in the caller `HashTable`.
 *      `none` if passed `key` is invalid or non-`Text` value
 *      is stored with it.
 */
public final function Text GetText(AcediaObject key)
{
    local Text result;

    result = Text(BorrowItem(key));
    if (result != none) {
        result.NewRef();
    }
    return result;
}

/**
 *  Returns `HashTable` item stored at key `key`. If key is invalid or
 *  stores a non-`HashTable` value, returns `none`.
 *
 *  Referred value must be stored as `HashTable`
 *  (or one of it's sub-classes) for this method to work.
 *
 *  @param  key Key of a `HashTable` item that caller `HashTable`
 *      has to return.
 *  @return `HashTable` value recorded with `key` in the caller
 *      `HashTable`. `none` if passed `key` is invalid or
 *      non-`HashTable` value is stored with it.
 */
public final function HashTable GetHashTable(AcediaObject key)
{
    local HashTable result;

    result = HashTable(BorrowItem(key));
    if (result != none) {
        result.NewRef();
    }
    return result;
}

/**
 *  Returns `ArrayList` item stored at key `key`. If key is invalid or
 *  stores a non-`ArrayList` value, returns `none`.
 *
 *  Referred value must be stored as `ArrayList`
 *  (or one of it's sub-classes) for this method to work.
 *
 *  @param  key Key of a `ArrayList` item that caller `HashTable`
 *      has to return.
 *  @return `ArrayList` value recorded with `key` in the caller
 *      `HashTable`. `none` if passed `key` is invalid or
 *      non-`ArrayList` value is stored with it.
 */
public final function ArrayList GetArrayList(AcediaObject key)
{
    local ArrayList result;

    result = ArrayList(BorrowItem(key));
    if (result != none) {
        result.NewRef();
    }
    return result;
}

defaultproperties
{
    iteratorClass = class'HashTableIterator'
    minimalCapacity = 0
    MINIMUM_SIZE    = 50
    MAXIMUM_SIZE    = 20000
    //  `MINIMUM_DENSITY * 2 < MAXIMUM_DENSITY` must hold for `HashTable`
    //  to work properly
    MINIMUM_DENSITY = 0.25
    MAXIMUM_DENSITY = 0.75
}