/**
 *      This should be considered an internal class and a detail of
 *  implementation.
 *      This is a data object that is used to store JSON data inside
 *  Unreal Engine's save packages (see `GameInfo` class, starting from
 *  `CreateDataObject()` method).
 *      Auxiliary data object that can store either a JSON array or an object in
 *  the local Acedia database. It is supposed to be saved and loaded
 *  to / from packages.
 *      Copyright 2021-2023 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 DBRecord extends Object
    perobjectconfig
    config(AcediaDB);

/**
 *  # How can JSON information be stored in per-config-objects?
 *      Standard way to store information locally would be by simply recording
 *  it inside a config file. This is enough for almost anything.
 *  Even something like ServerPerks' player database is implemented with just
 *  a per-config-objects: since it just stores a particular data per player -
 *  it can do with simply creating one storage object per player.
 *      We, however, want to store an arbitrary JSON object inside our database
 *  that can contain any other kind of JSON data and not just player's
 *  numeric stats. With some additional work this again can also be done with
 *  per-config-objects. For example, if we want to store a JSON object inside
 *  another JSON object - we can create both of them separately, give them some
 *  different arbitrary names and then make the first one refer to the second
 *  one by it's given name.
 *      This way we can create a config object for each JSON array/object and
 *  then store it's data as an array of plain types (same as ServerPerks' one):
 *  null, boolean, number and string can be stored as is and other
 *  JSON arrays/objects can be stored by their references.
 *
 *  # Why are we using data objects instead of per-object-configs?
 *      Despite everything described above, Acedia's local databases DO NOT use
 *  per-object-configs to store their data, opting for data objects and
 *  Unreal Engine's save packages instead.
 *      Data objects can be created, loaded and saved inside Unreal Engine's
 *  binary packages with methods available from `GameInfo` class (look them up
 *  starting from `CreateDataObject()` or browsing through
 *  [wiki](https://wiki.beyondunreal.com/Legacy:DataObject)).
 *      They can essentially act the same as per-object-configs, but have
 *  an advantage of allowing us to cheaply (execution time-wise) create/delete
 *  as many objects as we need and then update their package on the disk instead
 *  of calling `SaveConfig()` or `ClearConfig()` on them one-by-one. This both
 *  simplifies and speed up a bunch of necessary operations.
 *      They also seem to behave more predictably.
 *
 *  # Some terminology
 *      Acedia's objects (representing JSON values) that are getting loaded
 *  into the `DBRecord`s are called "objects". We then refer to their
 *  representation within `DBRecord`s as "items". For example, this class has
 *  two methods for conversion between the two: `ConvertObjectToItem()` and
 *  `ConvertItemToObject()`.
 *      Most other methods are:
 *      1.  either methods that actually perform Acedia's database queries;
 *      2.  or methods that provide safe and easy access to the `DBRecord`'s
 *          items array (like making sure to remove unneeded data objects).
 *      All of the methods that perform database query rely on the
 *  `ConvertPointer()` method that take `JSONPointer` and convert it into
 *  internal pointer representation that immediately points at `DBRecord` that
 *  represents referred data (or contains it).
 */

//  Name of the database package this object belongs to
var private string  package;
//  Does this record store a JSON array (`true`) or object (`false`)?
var private bool    isJSONArray;

//  `ToCollection()` and `EraseSelf()` methods make recursive calls on their
//  "sub-objects" (referred via name). If database was somehow damaged - a loop
//  of references can occur, leading to infinite recursive calls (which results
//  in a crash). These variable help to avoid that by preventing re-entry into
//  these methods for the same object.
var private bool lockToCollection;
var private bool lockEraseSelf;

/**
 *      We pack as much information into the type of the record:
 *  whether it's 'null', 'boolean', 'number', 'string' or reference to another
 *  `DBRecord`.
 *      If it's 'boolean', then record value in the type
 *  (`DBAT_False` / `DBAT_True`), if `number` record whether it's `int` or
 *  `float`.
 *      While JSON does not distinguish between `int` and `float`, we still
 *  have to pick one of these type when transferring JSON numeric value into
 *  UnrealScript, plus it's easier for us to store it in one of these types.
*/
enum DBDataType
{
    DBAT_Null,
    DBAT_False,
    DBAT_True,
    DBAT_Int,
    DBAT_Float,
    DBAT_String,
    //  We actually store the name of another `DBRecord` that represents either
    //  sub-array or sub-object.
    DBAT_Reference,
    //  Some integer values we might want to store won't fit into `int`, so we
    //  store them as `BigIntData`
    DBAT_BigInt,
};

/**
 *  Store JSON array / object as a bunch of values.
 *  Which variable is used to store value depends on the type `t`.
 */
struct StorageItem
{
    //  Determines whether variable's value is stored in `i`, `f` or `s`.
    var DBDataType  t;
    //  For JSON objects only (`isJSONArray == false`), stores the key of
    //  corresponding value.
    var string      k;
    var int         i;
    var float       f;
    //  For both `DBRecord` references and JSON strings
    var string      s;
    //  For storing `BigInt`'s `BigIntData` - last `byte` stores `negative`
    //  value
    var array<byte> b;
};
var private config array<StorageItem> storage;

var private const int       LATIN_LETTERS_AMOUNT;
var private const int       LOWER_A_CODEPOINT, UPPER_A_CODEPOINT;
var private const string    JSONPOINTER_NEW_ARRAY_ELEMENT;

/**
 *      Since `DBRecord` represents JSON array or object, we can use
 *  JSON pointers to refer to any sub-value inside it.
 *      However, JSON pointers are not convenient or efficient enough for that,
 *  so internally we use this struct that provides quick and easy access to
 *  any sub-value.
 */
struct DBRecordPointer
{
    //  `DBRecord` inside which referred value is directly stored.
    //  `record == none` automatically makes `DBRecordPointer` invalid.
    var DBRecord    record;
    //      Index in `record`'s `storage` variable that corresponds to
    //  referred (simple) value.
    //      Negative `index` values mean `record` itself is pointed at.
    //  To point at JSON array / object represented by a `DBRecord`, always set
    //  `record` to that record and `index` to negative value (e.g. `-1`).
    var int         index;
};

private final function bool IsValidPointer(DBRecordPointer pointer)
{
    return pointer.record != none;
}

private final function bool IsPointerToRecord(DBRecordPointer pointer)
{
    return (pointer.record != none && pointer.index < 0);
}

//  Auxiliary method serving as a simple constructor.
private final function DBRecordPointer MakeRecordPointer(
    DBRecord        record,
    optional int    index)
{
    local DBRecordPointer pointer;
    pointer.record  = record;
    pointer.index   = index;
    return pointer;
}

//  Converts `JSONPointer` into our internal representation.
private final function DBRecordPointer ConvertPointer(BaseJSONPointer jsonPointer)
{
    if (jsonPointer == none) {
        return MakeRecordPointer(none);
    }
    return ConvertPointerPath(jsonPointer, 0, jsonPointer.GetLength());
}

//  Produced out internal pointer representation `DBRecordPointer` to
//  the container that stores object, referred to by a given `JSONPointer`.
private final function DBRecordPointer ConvertContainerPointer(BaseJSONPointer jsonPointer)
{
    local DBRecordPointer pointer;
    if (jsonPointer == none) {
        return MakeRecordPointer(none);
    }
    pointer = ConvertPointerPath(jsonPointer, 0, jsonPointer.GetLength() - 1);
    if (!IsPointerToRecord(pointer)) {
        pointer.record = none;  //  invalidate pointer
    }
    return pointer;
}

//  Converts `JSONPointer` into internal `DBRecordPointer`.
//  Only uses sub-pointer: components from `startIndex` to `endIndex`.
private final function DBRecordPointer ConvertPointerPath(
    BaseJSONPointer pointer,
    int             startIndex,
    int             endIndex)
{
    local int           index;
    local StorageItem   nextElement;
    local DBRecord      nextRecord;
    local string        nextComponent;
    if (pointer == none) {
        return MakeRecordPointer(none);
    }
    //  We are done!
    if (startIndex >= endIndex) {
        return MakeRecordPointer(self, -1);
    }
    //  Use first available to us component to find next sub-object
    if (isJSONArray)
    {
        index = pointer.GetNumericComponent(startIndex);
        if (index < 0 || index >= storage.length) {
            return MakeRecordPointer(none); // fail: out-of-bounds index
        }
    }
    else
    {
        nextComponent = __().text.IntoString(pointer.GetComponent(startIndex));
        index = FindItem(nextComponent);
    }
    if (index < 0) {
        return MakeRecordPointer(none); // fail: missing key for component
    }
    nextElement = storage[index];
    if (nextElement.t != DBAT_Reference)
    {
        if (startIndex + 1 >= endIndex) {
            return MakeRecordPointer(self, index);
        }
        //  fail: found value cannot contain sub-values,
        //  but pointer is not exhausted
        return MakeRecordPointer(none);
    }
    nextRecord = LoadRecordFor(nextElement.s, package);
    if (nextRecord == none) {
        return MakeRecordPointer(none); // fail: bad database
    }
    //  Success for the component, do recursive call
    startIndex += 1;
    return nextRecord.ConvertPointerPath(pointer, startIndex, endIndex);
}

public static final function Global __()
{
    return class'Global'.static.GetInstance();
}

public static final function CoreGlobal __level()
{
    return class'CoreGlobal'.static.GetGenericInstance();
}

/**
 *  Method for creating a new `DBRecord` in a package named `dbPackageName`,
 *  picking an appropriate and unique name for it.
 *
 *  @param  dbPackageName   Name of the package new `DBRecord` must belong to.
 *  @return New `DBRecord`, created in specified package.
 *      `none` iff `dbPackageName == none`.
 */
public final static function DBRecord NewRecord(BaseText dbPackageName)
{
    if (dbPackageName == none) {
        return none;
    }
    return NewRecordFor(dbPackageName.ToString());
}

//  Auxiliary method that does what `NewRecord()` does, but for `string`
//  parameter. This makes it cheaper to call for internal use.
private final static function DBRecord NewRecordFor(string dbPackageName)
{
    local string    nextName;
    local DBRecord  recordCandidate;
    //  Try to generate new random name.
    //  This cycle can in theory be infinite. However in practice it will
    //  only run for one iteration (unless user messed with settings and
    //  set length of randomized names too low), since by default there is
    //  26^20 == 19,928,148,895,209,409,152,340,197,376 different
    //  random names and the chance of duplicate in infinitesimal.
    while (true)
    {
        nextName = GetRandomName();
        recordCandidate = LoadRecordFor(nextName, dbPackageName);
        if (recordCandidate != none) {
            continue;
        }
        recordCandidate = __level().unreal_api().GetGameType()
            .CreateDataObject(class'DBRecord', nextName, dbPackageName);
        recordCandidate.package = dbPackageName;
        return recordCandidate;
    }
    //  We cannot actually reach here
    return none;
}

public final static function DBRecord LoadRecord(
    BaseText recordName,
    BaseText dbPackageName)
{
    if (dbPackageName == none)  return none;
    if (recordName == none)     return none;

    return LoadRecordFor(   recordName.ToString(),
                            dbPackageName.ToString());
}

//  Auxiliary method that does what `LoadRecord()` does, but for `string`
//  parameter. This makes it cheaper to call for internal use.
private final static function DBRecord LoadRecordFor(
    string name,
    string package)
{
    return __level().unreal_api().GetGameType()
        .LoadDataObject(class'DBRecord', name, package);
}

private final static function string GetRandomName()
{
    local int       i;
    local int       length;
    local string    result;
    length = Max(1, class'LocalDBSettings'.default.randomNameLength);
    for (i = 0; i < length; i += 1) {
        result = result $ GetRandomLetter();
    }
    return result;
}

private final static function string GetRandomLetter()
{
    return Chr(Rand(default.LATIN_LETTERS_AMOUNT) + default.LOWER_A_CODEPOINT);
}

/**
 *  Loads Acedia's representation of JSON value stored at `pointer` inside
 *  the JSON object/array represented by the caller `DBRecord`.
 *
 *  @param  jsonPointer JSON pointer to the value to load
 *      (either simple, array or object one).
 *  @param  result      Loaded value will be recorded inside this variable.
 *      Set to `none` on failure.
 *  @param  makeMutable `false` if you want simple value to be recorded as
 *      immutable "boxes" (and `Text` for JSON strings) and `true` if you want
 *      them to be recorded as mutable "references"
 *      (`MutableText` for JSON strings).
 *  @return `true` if method successfully loaded JSON value and
 *      `false` otherwise. Failure can happen if passed `pointer` is invalid
 *      (either does not point at any existing value or is equal to `none`).
 */
public final function bool LoadObject(
    BaseJSONPointer     jsonPointer,
    out AcediaObject    result,
    bool                makeMutable)
{
    local int               itemIndex;
    local DBRecord          container;
    local DBRecordPointer   pointer;
    if (jsonPointer == none)        return false;
    pointer = ConvertPointer(jsonPointer);
    if (!IsValidPointer(pointer))   return false;

    if (IsPointerToRecord(pointer)) {
        result = pointer.record.ToCollection(makeMutable);
    }
    else
    {
        itemIndex = pointer.index;
        container = pointer.record;
        result = ConvertItemToObject(container.GetItem(itemIndex), makeMutable);
    }
    return true;
}

/**
 *  Saves Acedia's representation of JSON value at a `pointer` inside
 *  the JSON object/array represented by the caller `DBRecord`.
 *
 *  @param  jsonPointer JSON pointer to location at which to save the value.
 *      Only the last segment of the path will be created (if missing), the rest
 *      must already exist and will not be automatically created.
 *      If another value is already recorded at `pointer` - it will be erased.
 *  @param  newItem     New value to save at `pointer` inside
 *      the caller `DBRecord`.
 *  @return `true` if method successfully saved new JSON value and
 *      `false` otherwise. Failure can happen if passed `pointer` is invalid
 *      (either missing some necessary segments or is equal to `none`).
 */
public final function bool SaveObject(
    BaseJSONPointer jsonPointer,
    AcediaObject    newItem)
{
    local int               index;
    local string            itemKey;
    local DBRecord          directContainer;
    local Collection        newItemAsCollection;
    local DBRecordPointer   pointer;
    if (jsonPointer == none) {
        return false;
    }
    if (jsonPointer.IsEmpty())
    {
        //  Special case - rewriting caller `DBRecord` itself
        newItemAsCollection = Collection(newItem);
        if (newItemAsCollection == none) {
            return false;
        }
        EmptySelf();
        isJSONArray = (newItemAsCollection.class == class'ArrayList');
        FromCollection(newItemAsCollection);
        return true;
    }
    pointer = ConvertContainerPointer(jsonPointer);
    if (!IsValidPointer(pointer)) {
        return false;
    }
    directContainer = pointer.record;
    itemKey = __().text.IntoString(jsonPointer.Peek());
    if (directContainer.isJSONArray)
    {
        index = jsonPointer.PeekNumeric();
        if (index < 0 && itemKey == JSONPOINTER_NEW_ARRAY_ELEMENT) {
            index = directContainer.GetStorageLength();
        }
        if (index < 0) {
            return false;
        }
    }
    else {
        index = directContainer.FindItem(itemKey);
    }
    directContainer.SetItem(index, ConvertObjectToItem(newItem), itemKey);
    return true;
}

/**
 *  Removes Acedia's values stored in the database at `pointer` inside
 *  the JSON object/array represented by the caller `DBRecord`.
 *
 *  @param  jsonPointer JSON pointer to the value to remove
 *      (either simple, array or object one).
 *  @return `true` if method successfully removed JSON value and
 *      `false` otherwise. Failure can happen if passed `pointer` is invalid
 *      (either does not point at any existing value or equal to `none`).
 */
public final function bool RemoveObject(BaseJSONPointer jsonPointer)
{
    local int               itemIndex;
    local string            itemKey;
    local DBRecord          directContainer;
    local DBRecordPointer   containerPointer;
    if (jsonPointer == none)                return false;
    containerPointer = ConvertContainerPointer(jsonPointer);
    if (!IsValidPointer(containerPointer))  return false;

    directContainer = containerPointer.record;
    if (directContainer.isJSONArray) {
        itemIndex = jsonPointer.PeekNumeric();
    }
    else
    {
        itemKey = __().text.IntoString(jsonPointer.Peek());
        itemIndex = directContainer.FindItem(itemKey);
    }
    if (itemIndex >= 0)
    {
        directContainer.RemoveItem(itemIndex);
        return true;
    }
    return false;
}

/**
 *  Checks type of the JSON value stored at `pointer` inside
 *  the JSON object/array represented by the caller `DBRecord`.
 *
 *  @param  jsonPointer JSON pointer to the value for which type
 *      should be checked.
 *  @return `Database.DataType` that corresponds to the type of referred value.
 *      `JSON_Undefined` if value is missing or passed pointer is invalid.
 */
public final function LocalDatabaseInstance.DataType GetObjectType(
    BaseJSONPointer jsonPointer)
{
    local DBRecord          directContainer;
    local DBRecordPointer   pointer;
    if (jsonPointer == none)        return JSON_Undefined;
    pointer = ConvertPointer(jsonPointer);
    if (!IsValidPointer(pointer))   return JSON_Undefined;

    if (IsPointerToRecord(pointer))
    {
        if (pointer.record.isJSONArray) {
            return JSON_Array;
        }
        else {
            return JSON_Object;
        }
    }
    directContainer = pointer.record;
    switch (directContainer.GetItem(pointer.index).t)
    {
    case DBAT_Null:
        return JSON_Null;
    case DBAT_False:
    case DBAT_True:
        return JSON_Boolean;
    case DBAT_Int:
    case DBAT_Float:
        return JSON_Number;
    case DBAT_String:
        return JSON_String;
    }
    //  We should not reach here
    return JSON_Undefined;
}

/**
 *  Returns "size" of the JSON value stored at `pointer` inside
 *  the JSON object/array represented by the caller `DBRecord`.
 *
 *  For JSON arrays and objects it's the amount of stored elements.
 *  For other values it's considered undefined and method returns negative
 *  value instead.
 *
 *  @param  jsonPointer JSON pointer to the value for which method should
 *      return size.
 *  @return If `pointer` refers to the JSON array or object - amount of it's
 *      elements is returned. Otherwise returns `-1`.
 */
public final function int GetObjectSize(BaseJSONPointer jsonPointer)
{
    local DBRecordPointer pointer;
    if (jsonPointer == none) {
        return -1;
    }
    pointer = ConvertPointer(jsonPointer);
    if (IsPointerToRecord(pointer)) {
        return pointer.record.GetStorageLength();
    }
    return -1;
}

/**
 *  Returns keys of the JSON object stored at `pointer` inside
 *  the JSON object/array represented by the caller `DBRecord`.
 *
 *  @param  jsonPointer JSON pointer to the value for which method should
 *      return size.
 *  @return If `pointer` refers to the JSON object - all available keys.
 *      `none` otherwise (including case of JSON arrays).
 */
public final function ArrayList GetObjectKeys(BaseJSONPointer jsonPointer)
{
    local int                   i;
    local ArrayList             resultKeys;
    local array<StorageItem>    items;
    local DBRecord              referredObject;
    local DBRecordPointer       pointer;
    if (jsonPointer == none)            return none;
    pointer = ConvertPointer(jsonPointer);
    if (!IsValidPointer(pointer))       return none;
    if (!IsPointerToRecord(pointer))    return none;
    referredObject = pointer.record;
    if (referredObject.isJSONArray)     return none;

    resultKeys  = __().collections.EmptyArrayList();
    items       = referredObject.storage;
    for (i = 0; i < items.length; i += 1) {
        resultKeys.AddString(items[i].k);
    }
    return resultKeys;
}

/**
 *  Increments JSON value at a `pointer` inside the JSON object/array
 *  represented by the caller `DBRecord` by a given Acedia's value.
 *
 *  For "increment" operation description refer to `Database.IncrementData()`.
 *
 *  @param  jsonPointer JSON pointer to location at which to save the value.
 *      Only the last segment of the path might be created (if missing),
 *      the rest must already exist and will not be automatically created.
 *      If another value is already recorded at `pointer` - it will be erased.
 *  @param  object      Value by which to increment another value, stored at
 *      `pointer` inside the caller `DBRecord`.
 *  @return Returns query result that is appropriate for "increment" operation,
 *      according to `Database.IncrementData()` specification.
 */
public final function Database.DBQueryResult IncrementObject(
    BaseJSONPointer jsonPointer,
    AcediaObject    object)
{
    local int               index;
    local string            itemKey;
    local DBRecord          directContainer;
    local HashTable         objectAsHashTable;
    local DBRecordPointer   pointer;
    if (jsonPointer == none) {
        return DBR_InvalidPointer;
    }
    if (jsonPointer.IsEmpty())
    {
        //  Special case - incrementing caller `DBRecord` itself
        objectAsHashTable = HashTable(object);
        if (objectAsHashTable == none) {
            return DBR_InvalidData;
        }
        FromCollection(objectAsHashTable);
        return DBR_Success;
    }
    //      All the work will be done by the separate `IncrementItem()` method;
    //      But it is applied to the `DBRecord` that contains referred item,
    //  so we have to find it.
    pointer = ConvertContainerPointer(jsonPointer);
    if (!IsValidPointer(pointer)) {
        return DBR_InvalidPointer;
    }
    directContainer = pointer.record;
    itemKey = __().text.IntoString(jsonPointer.Peek());
    if (directContainer.isJSONArray)
    {
        index = jsonPointer.PeekNumeric();
        if (index < 0 && itemKey == JSONPOINTER_NEW_ARRAY_ELEMENT) {
            index = directContainer.GetStorageLength();
        }
        if (index < 0) {
            return DBR_InvalidPointer;
        }
    }
    else {
        index = directContainer.FindItem(itemKey);
    }
    if (directContainer.IncrementItem(index, object, itemKey)) {
        return DBR_Success;
    }
    return DBR_InvalidData;
}

private final function StorageItem GetItem(int index)
{
    local StorageItem emptyResult;
    if (index < 0)                  return emptyResult;
    if (index >= storage.length)    return emptyResult;

    return storage[index];
}

//      Negative `index` means that value will need to be appended to the end
//  of the `storage`.
//      Optionally lets you specify item's key (via `itemName`) for
//  JSON objects.
private final function SetItem(
    int             index,
    StorageItem     newItem,
    optional string itemName)
{
    local DBRecord      oldRecord;
    local StorageItem   oldItem;
    if (index < 0) {
        index = storage.length;
    }
    if (index < storage.length)
    {
        //  Clean up old value
        oldItem = storage[index];
        if (oldItem.t == DBAT_Reference)
        {
            oldRecord = LoadRecordFor(oldItem.s, package);
            if (oldRecord != none) {
                oldRecord.EmptySelf();
            }
            __level().unreal_api().GetGameType()
                .DeleteDataObject(class'DBRecord', oldItem.s, package);
        }
    }
    storage[index]      = newItem;
    storage[index].k    = itemName;
}

//      Auxiliary getter that helps us avoid referring to `storage` array
//  directly from `DBRecord` reference, which would cause unnecessary copying of
//  it's data.
private final function int GetStorageLength()
{
    return storage.length;
}

//      Auxiliary method for removing items from `storage` array that helps us
//  avoid referring to it directly from `DBRecord` reference, which would cause
//  unnecessary copying of it's data.
private final function RemoveItem(int index)
{
    local DBRecord      oldRecord;
    local StorageItem   oldItem;
    if (index >= storage.length)    return;
    if (index < 0)                  return;

    //  Clean up old value
    oldItem = storage[index];
    if (oldItem.t == DBAT_Reference)
    {
        oldRecord = LoadRecordFor(oldItem.s, package);
        if (oldRecord != none) {
            oldRecord.EmptySelf();
        }
        __level().unreal_api().GetGameType()
            .DeleteDataObject(class'DBRecord', oldItem.s, package);
    }
    storage.Remove(index, 1);
}

private final function int FindItem(string itemName)
{
    local int index;
    if (isJSONArray) {
        return -1;
    }
    for (index = 0; index < storage.length; index += 1)
    {
        if (storage[index].k == itemName) {
            return index;
        }
    }
    return -1;
}

//      Negative `index` means that `object` value needs to be appended to the
//  end of the `storage`, instead of incrementing an existing value.
//      Returns `true` if changes were successfully made and `false` otherwise.
private final function bool IncrementItem(
    int             index,
    AcediaObject    object,
    optional string itemName)
{
    local StorageItem itemToIncrement;
    if (index < 0)
    {
        index = storage.length;
        //  `itemToIncrement` is blank at this point and has type `DBAT_Null`,
        //  which will simply be rewritten by `IncrementItemByObject()`
        //  call later
        storage[index] = itemToIncrement;
    }
    else if (index < storage.length) {
        itemToIncrement = storage[index];
    }
    if (IncrementItemByObject(itemToIncrement, object))
    {
        //  Increment object cannot overwrite existing `DBRecord` with
        //  other value, so it's safe to skip cleaning check
        storage[index]      = itemToIncrement;
        storage[index].k    = itemName;
        return true;
    }
    return false;
}

/**
 *  Extracts JSON object or array data from caller `DBRecord` as either
 *  `HashTable` (for JSON objects) or `ArrayList` (for JSON arrays).
 *
 *  Type conversion rules in immutable case:
 *      1. 'null'       -> `none`;
 *      2. 'boolean'    -> `BoolBox`;
 *      3. 'number'     -> either `IntBox` or `FloatBox`, depending on
 *          what seems to fit better;
 *      4. 'string'     -> `Text`;
 *      5. 'array'      -> `ArrayList`;
 *      6. 'object'     -> `HashTable`.
 *
 *  Type conversion rules in mutable case:
 *      1. 'null'       -> `none`;
 *      2. 'boolean'    -> `BoolRef`;
 *      3. 'number'     -> either `IntRef` or `FloatRef`, depending on
 *          what seems to fit better;
 *      4. 'string'     -> `MutableText`;
 *      5. 'array'      -> `ArrayList`;
 *      6. 'object'     -> `HashTable`.
 *
 *  @param  makeMutable `false` if you want this method to produce
 *      immutable types and `true` otherwise.
 *  @return `HashTable` if caller `DBRecord` represents a JSON object
 *      and `ArrayList` if it represents JSON array.
 *      Returned collection must have all of it's keys deallocated before being
 *      discarded.
 *      `none` iff caller `DBRecord` was not initialized as either.
 */
public final function Collection ToCollection(bool makeMutable)
{
    local Collection result;
    if (lockToCollection) {
        return none;
    }
    lockToCollection = true;
    if (isJSONArray) {
        result = ToArrayList(makeMutable);
    }
    else {
        result = ToHashTable(makeMutable);
    }
    lockToCollection = false;
    return result;
}

//  Does not do any validation check, assumes caller `DBRecord`
//  represents an array.
private final function Collection ToArrayList(bool makeMutable)
{
    local int           i;
    local ArrayList     result;
    local AcediaObject  nextObject;
    result = __().collections.EmptyArrayList();
    for (i = 0; i < storage.length; i += 1)
    {
        nextObject = ConvertItemToObject(storage[i], makeMutable);
        result.AddItem(nextObject);
        __().memory.Free(nextObject);
    }
    return result;
}

//  Does not do any validation check, assumes caller `DBRecord`
//  represents an object.
private final function Collection ToHashTable(bool makeMutable)
{
    local int           i;
    local HashTable     result;
    local Text          nextKey;
    local AcediaObject  nextObject;
    result = __().collections.EmptyHashTable();
    for (i = 0; i < storage.length; i += 1)
    {
        nextKey = __().text.FromString(storage[i].k);
        nextObject = ConvertItemToObject(storage[i], makeMutable);
        result.SetItem(nextKey, nextObject);
        __().memory.Free(nextKey);
        __().memory.Free(nextObject);
    }
    return result;
}

/**
 *  Completely erases all data inside a caller `DBRecord`, recursively deleting
 *  all referred `DBRecord`.
 */
public final function EmptySelf()
{
    local int       i;
    local GameInfo  game;
    local DBRecord  subRecord;
    if (lockEraseSelf) {
        return;
    }
    lockEraseSelf = true;
    game = __level().unreal_api().GetGameType();
    for (i = 0; i < storage.length; i += 1)
    {
        if (storage[i].t != DBAT_Reference) continue;
        subRecord = LoadRecordFor(storage[i].s, package);
        if (subRecord == none)              continue;

        subRecord.EmptySelf();
        game.DeleteDataObject(class'DBRecord', string(subRecord.name), package);
    }
    storage.length = 0;
    lockEraseSelf = false;
}

/**
 *  Takes all available values from `source` and records them into caller
 *  `DBRecord`. Does not erase untouched old values, but will overwrite them
 *  in case of the conflict.
 *
 *      Can only convert items in passed collection that return `true` for
 *  `__().json.IsCompatible()` check. Any other values will be treated as `none`.
 *
 *  Only works as long as caller `DBRecord` has the same container type as
 *  `source`. `isJSONArray` iff `source.class == class'ArrayList` and
 *  `!isJSONArray` iff `source.class == class'HashTable`.
 *
 *  Values that cannot be converted into JSON will be replaced with `none`.
 *
 *  @param  source  `Collection` to write into the caller `DBRecord`.
 */
public final function FromCollection(Collection source)
{
    local ArrayList asArrayList;
    local HashTable asHashTable;
    asArrayList = ArrayList(source);
    asHashTable = HashTable(source);
    if (asArrayList != none && isJSONArray) {
        FromArrayList(asArrayList);
    }
    if (asHashTable != none && !isJSONArray) {
        FromHashTable(asHashTable);
    }
}

//  Does not do any validation check.
private final function FromArrayList(ArrayList source)
{
    local int           i, length;
    local AcediaObject  nextObject;
    length = source.GetLength();
    for (i = 0; i < length; i += 1)
    {
        nextObject = source.GetItem(i);
        storage[storage.length] = ConvertObjectToItem(nextObject);
        __().memory.Free(nextObject);
    }
}

//  Does not do any validation check.
private final function FromHashTable(HashTable source)
{
    local int                   i, originalStorageLength;
    local CollectionIterator    iter;
    local string                nextKey;
    local bool                  isNewKey;
    local AcediaObject          nextObject;
    originalStorageLength = storage.length;
    for (iter = source.Iterate(); !iter.HasFinished(); iter.Next())
    {
        if (iter.GetKey() == none) {
            continue;
        }
        nextKey = __().text.IntoString(BaseText(iter.GetKey()));
        isNewKey = true;
        for (i = 0; i < originalStorageLength; i += 1)
        {
            if (storage[i].k == nextKey)
            {
                isNewKey = false;
                break;
            }
        }
        if (isNewKey)
        {
            nextObject = iter.Get();
            SetItem(storage.length, ConvertObjectToItem(nextObject), nextKey);
            __().memory.Free(nextObject);
        }
    }
    iter.FreeSelf();
}

//  Converts `AcediaObject` into it's internal representation.
private final function StorageItem ConvertObjectToItem(AcediaObject data)
{
    local StorageItem   result;
    local DBRecord      newDBRecord;
    if (Text(data) != none)
    {
        result.t = DBAT_String;
        result.s = Text(data).ToString();
    }
    else if(Collection(data) != none)
    {
        result.t = DBAT_Reference;
        newDBRecord = NewRecordFor(package);
        newDBRecord.isJSONArray = (data.class == class'ArrayList');
        newDBRecord.FromCollection(Collection(data));
        result.s = string(newDBRecord.name);
    }
    else if (FloatBox(data) != none || FloatRef(data) != none)
    {
        result.t = DBAT_Float;
        if (FloatBox(data) != none) {
            result.f = FloatBox(data).Get();
        }
        else {
            result.f = FloatRef(data).Get();
        }
    }
    else if (IntBox(data) != none || IntRef(data) != none)
    {
        result.t = DBAT_Int;
        if (IntBox(data) != none) {
            result.i = IntBox(data).Get();
        }
        else {
            result.i = IntRef(data).Get();
        }
    }
    else if (BoolBox(data) != none || BoolRef(data) != none)
    {
        result.t = DBAT_False;
        if (BoolBox(data) != none && BoolBox(data).Get()) {
            result.t = DBAT_True;
        }
        if (BoolRef(data) != none && BoolRef(data).Get()) {
            result.t = DBAT_True;
        }
    }
    return result;
}

//  Converts internal data representation into `AcediaObject`.
private final function AcediaObject ConvertItemToObject(
    StorageItem item,
    bool        makeMutable)
{
    local DBRecord subRecord;
    switch (item.t) {
    case DBAT_False:
    case DBAT_True:
        if (makeMutable) {
            return __().ref.bool(item.t == DBAT_True);
        }
        else {
            return __().box.bool(item.t == DBAT_True);
        }
    case DBAT_Int:
        if (makeMutable) {
            return __().ref.int(item.i);
        }
        else {
            return __().box.int(item.i);
        }
    case DBAT_Float:
        if (makeMutable) {
            return __().ref.float(item.f);
        }
        else {
            return __().box.float(item.f);
        }
    case DBAT_String:
        if (makeMutable) {
            return __().text.FromStringM(item.s);
        }
        else {
            return __().text.FromString(item.s);
        }
    case DBAT_Reference:
        subRecord = LoadRecordFor(item.s, package);
        if (subRecord != none) {
            return subRecord.ToCollection(makeMutable);
        }
    default:
    }
    return none;
}

//      "Increments" internal data representation by value inside given
//  `AcediaObject`.
//      See `IncrementObject()` method for details.
private final function bool IncrementItemByObject(
    out StorageItem item,
    AcediaObject    object)
{
    local DBRecord itemRecord;
    if (object == none) {
        return (item.t == DBAT_Null);
    }
    if (item.t == DBAT_Null)
    {
        item = ConvertObjectToItem(object);
        return true;
    }
    else if (item.t == DBAT_String && Text(object) != none)
    {
        item.s $= Text(object).ToString();
        return true;
    }
    else if(item.t == DBAT_Reference && Collection(object) != none)
    {
        itemRecord = LoadRecordFor(item.s, package);
        if (itemRecord == none)
        {
            itemRecord = NewRecordFor(package); // DB was broken somehow
            item.s = string(itemRecord.name);
            itemRecord.isJSONArray = (object.class == class'ArrayList');
        }
        if (    (itemRecord.isJSONArray && object.class != class'ArrayList')
            ||  (   !itemRecord.isJSONArray
                &&  object.class != class'HashTable'))
        {
            return false;
        }
        itemRecord.FromCollection(Collection(object));
        return true;
    }
    else if (   (item.t == DBAT_False || item.t == DBAT_True)
            &&  (BoolBox(object) != none || BoolRef(object) != none))
    {
        if (BoolBox(object) != none && BoolBox(object).Get()) {
            item.t = DBAT_True;
        }
        if (BoolRef(object) != none && BoolRef(object).Get()) {
            item.t = DBAT_True;
        }
        return true;
    }
    return IncrementNumericItemByObject(item, object);
}

private final function bool IncrementNumericItemByObject(
    out StorageItem item,
    AcediaObject    object)
{
    local int   storedValueAsInteger, incrementAsInteger;
    local float storedValueAsFloat, incrementAsFloat;
    if (item.t != DBAT_Float && item.t != DBAT_Int) {
        return false;
    }
    if (!ReadNumericObjectInto(object, incrementAsInteger, incrementAsFloat)) {
        return false;
    }
    if (item.t == DBAT_Float)
    {
        storedValueAsInteger = int(item.f);
        storedValueAsFloat = item.f;
    }
    else
    {
        storedValueAsInteger = item.i;
        storedValueAsFloat = float(item.i);
    }
    //  Later we want to implement arbitrary precision arithmetic for storage,
    //  but for now let's just assume that if either value is a float -
    //  then user wants a float precision.
    if (    item.t == DBAT_Float || FloatBox(object) != none
        ||  FloatRef(object) != none)
    {
        item.t = DBAT_Float;
        item.f = storedValueAsFloat + incrementAsFloat;
        item.i = 0;
    }
    else
    {
        item.t = DBAT_Int;
        item.i = storedValueAsInteger + incrementAsInteger;
        item.f = 0;
    }
    return true;
}

private final function bool ReadNumericObjectInto(
    AcediaObject    object,
    out int         valueAsInt,
    out float       valueAsFloat)
{
    if (IntBox(object) != none || IntRef(object) != none)
    {
        if (IntBox(object) != none) {
            valueAsInt = IntBox(object).Get();
        }
        else {
            valueAsInt = IntRef(object).Get();
        }
        valueAsFloat = float(valueAsInt);
        return true;
    }
    if (FloatBox(object) != none || FloatRef(object) != none)
    {
        if (FloatBox(object) != none) {
            valueAsFloat = FloatBox(object).Get();
        }
        else {
            valueAsFloat = FloatRef(object).Get();
        }
        valueAsInt = int(valueAsFloat);
        return true;
    }
    return false;
}

//  Add storing bytes
defaultproperties
{
    LATIN_LETTERS_AMOUNT            = 26
    LOWER_A_CODEPOINT               = 97
    UPPER_A_CODEPOINT               = 65
    //  JSON Pointers allow using "-" as an indicator that element must be
    //  added at the end of the array
    JSONPOINTER_NEW_ARRAY_ELEMENT   = "-"
}