/**
 *  Object designed to allow for locally caching database's data and tracking
 *  all applied changes, even if database is yet to respond/rejected them.
 *  This includes tracking changes even *before* database's data is available,
 *  storing them inside as a series to edits to apply.
 *      Copyright 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 DBCache extends AcediaObject;

/**
 *  # `DBCache`
 *
 *  Object designed to allow for locally caching database's data and tracking
 *  all applied changes, even if database is yet to respond/rejected them.
 *  This includes tracking changes even *before* database's data is available,
 *  storing them inside as a series to edits to apply.
 *
 *  ## Usage
 *
 *      You can simply read and write JSON data with `Read(BaseJSONPointer)` and
 *  `Write(BaseJSONPointer, AcediaObject)` right after `DBCache`'s creation.
 *  Once real database's data has arrived, you can set it with `SetRealData()`.
 *      Data recorded before the `SetRealData()` call is an *approximation* and
 *  might not function as a real JSON value/database. Because `DBCache` doesn't
 *  yet know the real data in the database and even if you expect there to be
 *  a certain hierarchy of objects/arrays - `DBCache` cannot perform checks that
 *  they are there. This is why it simply lets you write any data at any path
 *  like "/A/B/C" in hopes that it data will be there after `SetRealData()`
 *  call.
 *      You can also "pre-create" such data by calling `Increment()` method with
 *  empty `Collection`s:
 *
 *  ```unrealscript
 *  local DBCache cache;
 *  local JSONPointer dataLocation;
 *  local HashTable emptyObject;
 *
 *  cache = DBCache(_.memory.Allocate(class'DBCache'));
 *  emptyObject = _.collections.EmptyHashTable();
 *  dataLocation = _.json.Pointer();
 *  cache.Increment(dataLocation, emptyObject);
 *  cache.Push(P("A"));
 *  cache.Increment(dataLocation, emptyObject);
 *  cache.Push(P("B"));
 *  cache.Increment(dataLocation, emptyObject);
 *  cache.Push(P("C"));
 *  _.memory.Free(emptyObject);
 *  _.memory.Free(dataLocation);
 *  ```
 *
 *      After `SetRealData()` edits you've made prior will be reapplied to
 *  provided data and you'll get report on what edits were attempted and what
 *  have failed:
 *
 *  ```unrealscript
 *  local array<DBCache.PendingEdit> completedEdits;
 *  // ...
 *  completedEdits = localCache.SetRealData(data);
 *  for (i = 0; i < completedEdits.length; i += 1)
 *  {
 *      if (completedEdits[i].successful) {
 *          //  Do something joyful!
 *      }
 *      else {
 *          //  Wail in your misery!
 *      }
 *      _.memory.Free(completedEdits[i].location);
 *      _.memory.Free(completedEdits[i].data);
 *  }
 *  ```
 *
 *  One more example of appending arrays using "-" JSON pointer component.
 *
 *  ```unrealscript
 *  local DBCache cache;
 *  local JSONPointer newDataLocation, arrayLocation;
 *  local Text data;
 *
 *  cache = DBCache(_.memory.Allocate(class'DBCache'));
 *  //  "-" in JSON pointer allows us to append to array from the end
 *  newDataLocation = _.json.Pointer_S("/array/-");
 *  data = _.text.FromString("Just new data!");
 *  cache.Write(newDataLocation, data);
 *
 *  //  Now add database's data: supposing `dbData` was a following JSON object:
 *  //  {"array": [1, 3, true], "tag": "db data!"}
 *  cache.SetRealData(dbData);
 *
 *  //  Now read changes back:t
 *  arrayLocation = _.json.Pointer_S("/array");
 *  cache.ReadData(arrayLocation);
 *  //  ^ returns `ArrayList` with following contents:
 *  [1, 3, true, "Just new data!"]
 *  ```
 *
 *  ## Implementation
 *
 *      Cache can be in two distinct states: before (`cachedData` is `false`)
 *  and after (`cachedData` is `true`) obtaining database's actual data.
 *  In the second state it simply stores `AcediaObject` that represents stored
 *  JSON value and applies all changes to it directly / reads from it directly.
 *      In case database's data wasn't yet obtained - stores all valid `Write()`
 *  requests as an array of edits `pendingEdits`. Any `Read()` causes us to go
 *  through that array until we:
 *
 *      1.  Find an edit that could've written a data user;
 *      2.  Obtain necessary data from that edit (we might want some folded
 *          sub-object);
 *      3.  Reapply all later edits to that data and return it.
 *
 *      We also use similar process when adding new edits during `Write()`:
 *  if we know for a fact that at "/a/b/c" is a non-container
 *  (like a JSON string or number), then we simply reject any writes to its
 *  sub-data like "/a/b/c/d", since it is impossible to write anything inside.
 *  This also applies to the JSON arrays if we want to write into them using
 *  non-numeric keys.
 *      Additionally, for the sake of efficiency, `DBCache` erases old edits in
 *  case their data gets completely overwritten by new ones: if we first write
 *  something inside "/array/1" and then rewrite the whole "/array" - we no
 *  longer need to store the first edit for anything.
 *
 *  ## Remarks
 *
 *  Before `SetRealData()` is called, the collection inside `DBCache` is mostly
 *  faked. In most practical cases it shouldn't noticeable and the most notable
 *  issue one can stumble on is that `DBCache` allows to write data at paths, it
 *  is not sure even exist, leading to weird behavior:
 *
 *  ```unrealscript
 *  local DBCache cache;
 *  local JSONPointer newDataLocation, objectLocation;
 *  local Text data;
 *
 *  cache = DBCache(_.memory.Allocate(class'DBCache'));
 *  //  "-" in JSON pointer allows us to append to array from the end
 *  newDataLocation = _.json.Pointer_S("/subObject/field");
 *  data = _.text.FromString("Just new data!");
 *  cache.Write(newDataLocation, data);
 *
 *  //  Without adding database's data with `SetRealData()` we get:
 *  objectLocation = _.json.Pointer_S("/subObject");
 *  //  returns `none`, since we have no info about data at that path
 *  cache.ReadData(objectLocation);     //  none
 *  //  But still remembers that this is "Just new data!"
 *  cache.ReadData(newDataLocation);    //  "Just new data!"
 *  ```
 */

enum DBCacheEditType
{
    DBCET_Write,
    DBCET_Increment,
    DBCET_Remove
};

//  Represents a single edit made as a result of `Write()` call
struct PendingEdit
{
    var public DBCacheEditType  type;
    var public JSONPointer      location;
    var public AcediaObject     data;
    var public bool             successful;
};
//      All valid edits made so far (minus impossible and overwritten ones) in
//  order they were made: the lower index, the older the edit.
var private array<PendingEdit> pendingEdits;

//  Was data already cached?
//  We cannot simply use `cachedData == none`, since `none` is a valid value.
var private bool            isDataCached;
//  Data, obtained from the database
var private AcediaObject    cachedData;

protected function Finalizer()
{
    local int i;

    for (i = 0; i < pendingEdits.length; i += 1) {
        FreePendingWrite(pendingEdits[i]);
    }
    pendingEdits.length = 0;
    _.memory.Free(cachedData);
    cachedData = none;
    isDataCached = false;
}

/**
 *  Reads data from `DBCache` stored at the pointer `location`.
 *  If no data is recorded at `location`, returns `none`.
 *
 *  NOTE: If the real database's data wasn't yet set with `SetRealData()`,
 *  then this method can return `none` for path like "/a/b", even if value
 *  "/a/b/c" was already set. This is because `DBCache` doesn't try to guess
 *  types of containers on the way to the recorded data: if 'b' were to
 *  be numeric - then we'd have no idea whether it is an array of an object.
 *
 *  @param  location    Location inside `DBCache`'s stored data, from which to
 *      read data of interest.
 *  @return Data stored at location given by `location`, `none` if nothing is
 *      stored there.
 */
public final function AcediaObject Read(BaseJSONPointer location)
{
    local Collection cachedCollection;

    if (location == none) {
        return none;
    }
    if (!isDataCached) {
        return ReadPending(location);
    }
    if (location.IsEmpty()) {
        return _.json.Copy(cachedData);
    }
    //  For non-empty pointers, `cachedCollection` must be a `Collection`
    cachedCollection = Collection(cachedData);
    if (cachedCollection != none) {
        return cachedCollection.GetItemByJSON(location);
    }
    return none;
}

/**
 *  Writes data into `DBCache` at the given location.
 *
 *  This method can work differently depending on whether `SetRealData()` call
 *  was already made:
 *
 *      1.  Before the `SetRealData()` call it basically knows nothing about
 *          object inside database (the real data) and will freely write at any
 *          location as long as that operation won't contradict previous edits
 *          (it will attempt to recognize and prevent removing sub-values inside
 *          null, booleans, strings and numbers or non-numeric keys into
 *          arrays).
 *      2.  After the `SetRealData()` call it already knows what data was stored
 *          inside before edits were made and would only allow to write data at
 *          "/path/to/value" if "/path/to" (i.e. path without its last
 *          component) corresponds to an appropriate collection (JSON object or
 *          JSON array if last component is numeric or "-").
 *
 *  @param  location    Location into which to write new data.
 *  @param  data        Data to write, expected to consist only of
 *      the JSON-compatible types. It will be copied with `_.json.Copy()`,
 *      so the reference won't be kept.
 *  @return `true` if write was successful and `false` otherwise. Note that
 *      operations made before `SetRealData()` can be reported as successful,
 *      but then rejected after the real data is set if they're incompatible
 *      with its structure (@see `SetRealData()` for more information).
 */
public final function bool Write(BaseJSONPointer location, AcediaObject data)
{
    local Collection cachedCollection;

    if (location == none) {
        return false;
    }
    if (!isDataCached) {
        return AddPendingEdit(location, data, DBCET_Write);
    }
    if (location.IsEmpty())
    {
        _.memory.Free(cachedData);
        cachedData = _.json.Copy(data);
        return true;
    }
    cachedCollection = Collection(cachedData);
    //      At this point `EditJSONCollection()`'s contract of
    //  `cachedCollection != none` and `location` isn't `none` or empty is
    //  satisfied.
    if (cachedCollection != none)
    {
        return EditJSONCollection(
            cachedCollection,
            location,
            data,
            DBCET_Write);
    }
    return false;
}

/**
 *  Removes data from `DBCache` at the given location.
 *
 *  This method can work differently depending on whether `SetRealData()` call
 *  was already made:
 *
 *      1.  Before the `SetRealData()` call it basically knows nothing about
 *          object inside database (the real data) and will freely perform
 *          removal at any location as long as that operation won't obviously
 *          contradict previous edits (it will attempt to recognize and prevent
 *          removing sub-values inside null, booleans, strings and numbers or
 *          non-numeric keys into arrays).
 *      2.  After the `SetRealData()` call it already knows what data was stored
 *          inside before edits were made and would only allow to write data at
 *          "/path/to/value" if "/path/to" (i.e. path without its last
 *          component) corresponds to an appropriate collection (JSON object or
 *          JSON array if last component is numeric or "-").
 *
 *  @param  location    Location from which to remove all data.
 *  @return `true` if removal was successful and `false` otherwise. Note that
 *      operations made before `SetRealData()` can be reported as successful,
 *      but then rejected after the real data is set if they're incompatible
 *      with its structure (@see `SetRealData()` for more information).
 */
public final function bool Remove(BaseJSONPointer location)
{
    local Collection cachedCollection;

    if (location == none) {
        return false;
    }
    if (!isDataCached) {
        return AddPendingEdit(location, none, DBCET_Remove);
    }
    if (location.IsEmpty())
    {
        _.memory.Free(cachedData);
        cachedData = none;
        return true;
    }
    cachedCollection = Collection(cachedData);
    //      At this point `EditJSONCollection()`'s contract of
    //  `cachedCollection != none` and `location` isn't `none` or empty is
    //  satisfied.
    if (cachedCollection != none)
    {
        return EditJSONCollection(
            cachedCollection,
            location,
            none,
            DBCET_Remove);
    }
    return false;
}

/**
 *  Increments data inside `DBCache` at the given location.
 *
 *  @see `_.json.Increment()`.
 *
 *  This method can work differently depending on whether `SetRealData()` call
 *  was already made:
 *
 *      1.  Before the `SetRealData()` call it basically knows nothing about
 *          object inside database (the real data) and will freely increment at
 *          any location as long as that operation won't contradict previous
 *          edits (it will attempt to recognize and prevent removing sub-values
 *          inside null, booleans, strings and numbers or non-numeric keys into
 *          arrays).
 *      2.  After the `SetRealData()` call it already knows what data was stored
 *          inside before edits were made and would only allow to incrementing
 *          data at "/path/to/value" if "/path/to" (i.e. path without its last
 *          component) corresponds to an appropriate collection (JSON object or
 *          JSON array if last component is numeric or "-").
 *
 *  @param  location    Location of the value to increment with new, given data.
 *  @param  data        Data to increment with, expected to consist only of
 *      the JSON-compatible types. It will be copied with `_.json.Copy()`,
 *      so the reference won't be kept.
 *  @return `true` if increment was successful and `false` otherwise. Note that
 *      operations made before `SetRealData()` can be reported as successful,
 *      but then rejected after the real data is set if they're incompatible
 *      with its structure (@see `SetRealData()` for more information).
 */
public final function bool Increment(BaseJSONPointer location, AcediaObject data)
{
    local AcediaObject  incrementedRoot;
    local Collection    cachedCollection;

    if (location == none) {
        return false;
    }
    if (!isDataCached) {
        return AddPendingEdit(location, data, DBCET_Increment);
    }
    cachedCollection = Collection(cachedData);
    if (cachedCollection != none) {
        return EditJSONCollection(
            cachedCollection,
            location,
            data,
            DBCET_Increment);
    }
    else if (location.IsEmpty())
    {
        incrementedRoot = _.json.increment(cachedData, data);
        _.memory.Free(cachedData);
        cachedData = incrementedRoot;
        return true;
    }
    return false;
}

/**
 *  Checks whether `SetRealData()` was called.
 *
 *  @return `true` if `SetRealData()` was called and `DBCache` is in
 *      second mode, working on the given, cached data instead of the edits.
 *      `false` otherwise.
 */
public final function bool IsRealDataSet()
{
    return isDataCached;
}

/**
 *  Sets real data that `DBCache` must use as basis for all of its changes.
 *
 *  Any valid changes (for which `Write()` has previously returned `true` and
 *  which weren't overwritten by later changes) will be reapplied to given
 *  object and whether applying each edit ended in success of failure will be
 *  reported in the returned value.
 *
 *  Can only be called once, all subsequent call will do nothing and will return
 *  empty array.
 *
 *  @param  realData    Data to use as basis, expected to consist only of
 *      the JSON-compatible types. It will be copied with `_.json.Copy()`,
 *      so the reference won't be kept.
 *  @return All valid edits made so far (minus impossible and overwritten ones)
 *      in order they were made: the lower index, the older the edit. Includes
 *      a boolean flag that indicates whether each particular edit was
 *      successfully applied to given data.
 */
public final function array<PendingEdit> SetRealData(AcediaObject realData)
{
    local int                   i;
    local Collection            cachedCollection;
    local array<PendingEdit>    pendingEditsCopy;

    if (isDataCached) {
        return pendingEdits;
    }
    cachedData = _.json.Copy(realData);
    for (i = 0; i < pendingEdits.length; i += 1)
    {
        cachedCollection = Collection(cachedData);
        if (    pendingEdits[i].location.IsEmpty()
            &&  pendingEdits[i].type != DBCET_Increment)
        {
            //  Generally `pendingEdits[i].location.IsEmpty()` should be `true`
            //  for non-incrementing operations only for one index, since all
            //  edits would get overwritten be overwritten by the newest one,
            //  but let's do these changes just in case.
            _.memory.Free(cachedData);
            if (pendingEdits[i].type == DBCET_Write)
            {
                pendingEdits[i].successful = true;
                cachedData = _.json.Copy(pendingEdits[i].data);
            }
            else //  (pendingEdits[i].type == DBCET_Remove)
            {
                pendingEdits[i].successful = (cachedData != none);
                cachedData = none;
            }
        }
        else if (cachedCollection != none)
        {
            //  Any other edits affect sub-objects and can, therefore, only be
            //  applied to `Collection`s (JSON objects and arrays).
            pendingEdits[i].successful = EditJSONCollection(
                cachedCollection,
                pendingEdits[i].location,
                pendingEdits[i].data,
                pendingEdits[i].type);
        }
    }
    pendingEditsCopy = pendingEdits;
    pendingEdits.length = 0;
    isDataCached = true;
    return pendingEditsCopy;
}

//  For reading data when before the `SetRealData()` call.
private final function AcediaObject ReadPending(BaseJSONPointer location)
{
    local int nextEditIndex;
    local int newestOverridingEdit;

    if (location == none) {
        return none;
    }
    //      Go from the newest to the latest edit and find newest edit that
    //  *completely overwrites* data at `location`.
    //      This can be any newest pointer that serves as prefix to `location`
    //  that *writes* or *removes* data.
    //      If there are only *append* edits, we need to take the oldest one,
    //  since it is possible for several appending edits to stuck on top of each
    //  other.
    newestOverridingEdit = -1;
    nextEditIndex = pendingEdits.length - 1;
    while (nextEditIndex >= 0)
    {
        
        if (location.StartsWith(pendingEdits[nextEditIndex].location))
        {
            newestOverridingEdit = nextEditIndex;
            if (pendingEdits[nextEditIndex].type != DBCET_Increment) {
                break;
            }
        }
        nextEditIndex -= 1;
    }
    if (newestOverridingEdit >= 0) {
        return ReconstructFromEdit(location, newestOverridingEdit);
    }
    return none;
}

//      Takes data from the given edit from `pendingEdits` as a basis and
//  reapplies newer applicable edits to it.
//      Assumes `location` is not `none`.
//      Assumes `pendingEdits[editIndex].location` is prefix of `location`.
private final function AcediaObject ReconstructFromEdit(
    BaseJSONPointer location,
    int             editIndex)
{
    local int           startIndex;
    local AcediaObject  result;
    local Collection    outerCollection;

    outerCollection = Collection(pendingEdits[editIndex].data);
    if (location.GetLength() == pendingEdits[editIndex].location.GetLength()) {
        //  In case pending edit was made at the exactly `location`, simply copy
        //  its data, since `location` is pointing right at it.
        result = _.json.Copy(pendingEdits[editIndex].data);
    }
    else if (outerCollection != none)
    {
        //      Otherwise `location` is pointing deeper than
        //  `pendingEdits[editIndex].location` and we need to return
        //  a sub-object. This means that `data` has to be a `Collection`.
        //      First find that `Collection` (stored inside `outerCollection`),
        //  pointed by `location` inside `pendingEdits[editIndex].data`
        //  (with removed `pendingEdits[editIndex].location` prefix).
        startIndex = pendingEdits[editIndex].location.GetLength();
        result = ApplyPointer(
            outerCollection,
            location,
            startIndex,
            location.GetLength());
        //  We can safely release our rights to keep `result` reference and
        //  still use it, since it is still stored `outerCollection` and won't
        //  get deallocated.
        _.memory.Free(result);
        //      `startIndex` is an `out` variable that records how far
        //  `ApplyPointer()` was able to travel along `location`.
        //      We are only successful if we reached the end.
        if (startIndex == location.GetLength()) {
            result = _.json.Copy(result);
        }
        else {
            result = none;
        }
    }
    ApplyCorrectingWrites(result, location, editIndex + 1);
    return result;
}

//      Attempts to apply all sufficiently new (at least with index
//  `startIndex`) edits to the `target`.
//      Assumes `locationToCorrect` in not `none`.
private final function ApplyCorrectingWrites(
    out AcediaObject    target,
    BaseJSONPointer     locationToCorrect,
    int                 startIndex)
{
    local int           i;
    local Collection    targetAsCollection;
    local JSONPointer   subLocation, nextLocation;

    if (target == none) {
        return;
    }
    for (i = startIndex; i < pendingEdits.length; i += 1)
    {
        nextLocation = pendingEdits[i].location;
        if (!nextLocation.StartsWith(locationToCorrect)) {
            continue;
        }
        targetAsCollection = Collection(target);
        //  `Collection`s (JSON arrays or objects) have to be handled
        //  differently, since we might need to change values stored deep within
        //  the `Collection`, while for other variables we can change `target`
        //  directly.
        if (targetAsCollection != none)
        {
            subLocation = nextLocation.Copy(locationToCorrect.GetLength());
            EditJSONCollection(
                Collection(target),
                subLocation,
                pendingEdits[i].data,
                pendingEdits[i].type);
            subLocation.FreeSelf();
        }
        else if (nextLocation.GetLength() == locationToCorrect.GetLength())
        {
            EditJSONSimpleValue(
                target,
                pendingEdits[i].data,
                pendingEdits[i].type);
        }
    }
}

//  Applies operation of type `editType` to the given object.
//  Assumes that `target` isn't `none`.
//  Makes a copy of the `value`.
private final function bool EditJSONSimpleValue(
    out AcediaObject    target,
    AcediaObject        value,
    DBCacheEditType     editType)
{
    local AcediaObject newTarget;

    if (editType == DBCET_Write) {
        newTarget = _.json.Copy(value);
    }
    else if (editType == DBCET_Remove) {
        newTarget = none;
    }
    else
    {
        newTarget = _.json.Increment(target, value);
        if (newTarget == none) {
            return false;
        }
    }
    _.memory.Free(target);
    target = newTarget;
}

//      Applies operation of type `editType` to the object stored inside
//  given `Collection`, given by `location`.
//      Makes a copy of the `value`.
//      Assumes that `location` can only be an empty pointer if `editType` is
//  `DBCET_Increment`.
//      Assumes that `target` isn't `none`.
private final function bool EditJSONCollection(
    Collection      target,
    BaseJSONPointer location,
    AcediaObject    value,
    DBCacheEditType editType)
{
    local bool                  success;
    local Text                  key;
    local ArrayList             arrayCollection;
    local HashTable             objectCollection;
    local Collection            innerCollection;
    local MutableJSONPointer    poppedLocation;
    local AcediaObject          valueCopy;

    //  Empty pointer is only allowed if we're incrementing;
    if (location.IsEmpty())
    {
        return (editType == DBCET_Increment
            && IncrementCollection(target, value));
    }
    //      First get `Collection` that stores data, pointed by `location`
    //  (which is data pointed by `location` without the last segment).
    //      Last segment will serve as a key in that `Collection`, so also
    //  keep it.
    poppedLocation = location.MutableCopy();
    key = poppedLocation.Pop();
    innerCollection = target.GetCollectionByJSON(poppedLocation);
    //  Then, depending on the collection, get the actual data
    arrayCollection     = ArrayList(innerCollection);
    objectCollection    = HashTable(innerCollection);
    valueCopy           = _.json.Copy(value);
    if (arrayCollection != none)
    {
        success = EditArrayList(
            arrayCollection,
            key,
            value,
            editType,
            location.PeekNumeric());
    }
    if (objectCollection != none) {
        success = EditHashTable(objectCollection, key, value, editType);
    }
    _.memory.Free(innerCollection);
    _.memory.Free(poppedLocation);
    _.memory.Free(key);
    _.memory.Free(valueCopy);
    return success;
}

//  Assumes `collection != none` and `key != none`
//  Assumes value is already copied and won't be stored anywhere else
private final function bool EditArrayList(
    ArrayList       collection,
    Text            key,
    AcediaObject    value,
    DBCacheEditType editType,
    int             numericKey)
{
    local AcediaObject incrementedValue;

    //  Only valid case of `numericKey < 0` is when `key` is "-", which can only
    //  be used with `DBCET_Write` to append to `collection`
    if (numericKey < 0 && editType != DBCET_Write) {
        return false;
    }
    if (editType == DBCET_Write)
    {
        if (numericKey >= 0) {
            collection.SetItem(numericKey, value);
        }
        else if (key.IsEqual(P("-"))) {
            collection.AddItem(value);
        }
        else {
            return false;
        }
    }
    else if (editType == DBCET_Remove)
    {
        if (numericKey >= collection.GetLength()) {
            return false;
        }
        collection.RemoveIndex(numericKey);
        return true;
    }
    else // if (editType == DBCET_Increment)
    {
        if (value == none)
        {
            if (numericKey >= collection.GetLength()) {
                collection.SetItem(numericKey, none);
            }
            //  Incrementing by `none` is a success for any reachable value
            //  (including a missing one, if the immediate parent is present)
            return true;
        }
        incrementedValue =
            EfficientIncrement(collection.GetItem(numericKey), value);
        if (incrementedValue != none) {
            collection.SetItem(numericKey, incrementedValue);
        }
        _.memory.Free(incrementedValue); // `none` or moved into `collection`
        return (incrementedValue != none);
    }
    return true;
}

//  Assumes `collection != none` and `key != none`
//  Assumes value is already copied and won't be stored anywhere else
private final function bool EditHashTable(
    HashTable       collection,
    Text            key,
    AcediaObject    value,
    DBCacheEditType editType)
{
    local AcediaObject incrementedValue;

    if (editType == DBCET_Write) {
        collection.SetItem(key, value);
    }
    else if (editType == DBCET_Remove)
    {
        if (!collection.HasKey(key)) {
            return false;
        }
        collection.RemoveItem(key);
        return true;
    }
    else // if (editType == DBCET_Increment)
    {
        if (value == none)
        {
            if (!collection.HasKey(key)) {
                collection.SetItem(key, none);
            }
            //  Incrementing by `none` is a success for any reachable value
            //  (including a missing one, if the immediate parent is present)
            return true;
        }
        incrementedValue = EfficientIncrement(collection.GetItem(key), value);
        if (incrementedValue != none) {
            collection.SetItem(key, incrementedValue);
        }
        _.memory.Free(incrementedValue); // `none` or moved into `collection`
        return (incrementedValue != none);
    }
    return true;
}

//      This method is supposed to be more efficient than
//  `_.json.Increment()` because it can skip copying `valueToIncrement`
//  in case it's a collection and append to it directly.
//      Assumes `increment` is not `none`.
//      Returning `none` means increment has failed (could only happen possible
//  if `increment` is `none`, which is impossible)
private final function AcediaObject EfficientIncrement(
    /*take*/    AcediaObject valueToIncrement,
                AcediaObject increment)
{
    local AcediaObject incrementedValue;

    if (valueToIncrement == none) {
        return _.json.Copy(increment);
    }
    //  This is the "efficient part": we first try to directly append
    //  `increment` to `valueToIncrement`, since it can avoid unnecessary
    //  copying of huge collections
    if (    Collection(valueToIncrement) != none
        &&  valueToIncrement.class == increment.class)
    {
        //  If we're inside, then we are sure that both arguments are either
        //  `ArrayList`s or `HashTable`s (and not `none`!)
        IncrementCollection(valueToIncrement, increment);
        //  We reuse `valueToIncrementAsHashTable`, so simply return reference
        //  we took ownership of
        return valueToIncrement;
    }
    //  Since all correct `Collection` cases were handled above, we only need to
    //  do the normal, "inefficient" incrementing when both arguments aren't
    //  `Collection`s
    if (Collection(valueToIncrement) == none && Collection(increment) == none) {
        incrementedValue = _.json.Increment(valueToIncrement, increment);
    }
    //  We do not reuse either `valueToIncrement`, so we should release it
    _.memory.Free(valueToIncrement);
    //  This will be `none` in case `_.json.Increment()` wasn't called
    return incrementedValue;

}

//      Increments `valueToIncrement` (changing its value) by `increment`.
//      Only does work if both arguments are the same type of `Collection`.
//      Returns `true` if it actually incremented `valueToIncrement` and `false`
//  otherwise.
//      If arguments have different types - does nothing.
private final function bool IncrementCollection(
    AcediaObject valueToIncrement,
    AcediaObject increment)
{
    local ArrayList valueToIncrementAsArrayList, incrementAsArrayList;
    local HashTable valueToIncrementAsHashTable, incrementAsHashTable;

    valueToIncrementAsArrayList = ArrayList(valueToIncrement);
    if (valueToIncrementAsArrayList != none)
    {
        incrementAsArrayList = ArrayList(increment);
        if (incrementAsArrayList != none)
        {
            valueToIncrementAsArrayList.Append(incrementAsArrayList);
            return true;
        }
    }
    valueToIncrementAsHashTable = HashTable(valueToIncrement);
    if (valueToIncrementAsHashTable != none)
    {
        incrementAsHashTable = HashTable(increment);
        if (incrementAsHashTable != none)
        {
            valueToIncrementAsHashTable.Append(incrementAsHashTable);
            return true;
        }
    }
    return false;
}

//  For writing data when before the `SetRealData()` call.
//  Assumes `location` isn't `none`.
private final function bool AddPendingEdit(
    BaseJSONPointer location,
    AcediaObject    data,
    DBCacheEditType type)
{
    local int           i, index;
    local bool          isIncrementing;
    local AcediaObject  leafItem;
    local PendingEdit   newWrite;

    //      We basically just want to add new edit struct into `pendingEdits`
    //  array, but there's three additional consideration:
    //
    //      1.  Some edits can be decided to be *impossible*: if, at the earlier
    //          stage, we wrote a simple type (not JSON array or object) at
    //          some location "/a/b" and then try to write a sub-object at
    //          the longer path "/a/b/c". This action is impossible and should
    //          be rejected outright, to prevent reading methods from reading
    //          data that will obviously be rejected on real object.
    //          NOTE: We only catch some of such cases, checking against
    //          the very first edit in the chain that build up data at
    //          `location`, since checking checking against data written by
    //          the other edits on top would require us to do too much work.
    //      2.  Some new edits can overwrite older ones: if we wrote something
    //          at location "/a/b/c" and then write something at "/a/b" - we can
    //          completely disregard and remove older edit at "/a/b/c".
    //      3.  Whenever we're doing incrementing edit, we want to be able to
    //          keep several of such edits at once (possibly on top of some
    //          writing edit), without them overwriting each other (as per
    //          previous point). There is also a special case for when we're
    //          writing into JSON array with pointer ending in "-" - it
    //          indicates adding a new element, which also makes it incrementing
    //          operation.

    //      This variable will store whether `location` ends with "-" (writing
    //  operation corresponds to what was discussed in point 3)
    isIncrementing = (type == DBCET_Increment)
        || IsPointerAppendingToArray(location);
    while (i < pendingEdits.length)
    {
        if (pendingEdits[i].location.StartsWith(location))
        {
            //  Here we're in situation described in point 2, where new edit
            //  will overwrite `pendingEdits[i].location`.
            /*isSameLength =
                (location.GetLength() == pendingEdits[i].location.GetLength());*/
            //      Since `location` is prefix for `pendingEdits[i].location`,
            //  then it is either shorter or the same length. Same length here
            //  also means that these pointers are *identical*.
            //      We can prevent removal of the old rule only in situation of
            //  point 3, where new edit wants to increment an item (guaranteed
            //  by `isIncrementing`).
            //
            //  NOTE: Here we make no check for whether we're writing into
            //  an object, which can result in us keeping several redundant
            //  edits. But we expect this to be a rare case and a fine trade off
            //  for skipping additional costly checks.
            if (!isIncrementing)
            {
                FreePendingWrite(pendingEdits[i]);
                pendingEdits.Remove(i, 1);
                continue;
            }
        }
        else if (pendingEdits[i].type == DBCET_Write
            && location.StartsWith(pendingEdits[i].location))
        {
            //      Here we perform checks described in point 1: follow along
            //  the `location` pointer as far as possible and check if last
            //  known structure can at least in theory be explored by the rest
            //  of `location` after database's real data is loaded and set.
            index = pendingEdits[i].location.GetLength();
            leafItem = ApplyPointer(
                pendingEdits[i].data,
                location,
                index,
                location.GetLength() - 1);
            if (    Collection(leafItem) == none
                ||  !IsKeyAcceptable(Collection(leafItem), location, index))
            {
                return false;
            }
        }
        i += 1;
    }
    //  After all checks have passed and all older irrelevant edits were
    //  filtered out - add the new one.
    newWrite.location = location.Copy();
    newWrite.data = _.json.Copy(data);
    newWrite.type = type;
    pendingEdits[pendingEdits.length] = newWrite;
    return true;
}

//      Checks if `pointer`'s last component is "-", which denotes appending
//  new item to the JSON array.
//      Assumes `pointer != none`.
private final function bool IsPointerAppendingToArray(BaseJSONPointer pointer)
{
    local int lastComponentIndex;

    lastComponentIndex = pointer.GetLength() - 1;
    if (!pointer.IsComponentArrayApplicable(lastComponentIndex)) {
        return false;
    }
    return (pointer.GetNumericComponent(lastComponentIndex) < 0);
}

//      Checks whether given key is acceptable for given collection.
//      To avoid unnecessary copying the key is specified as a component of
//  JSON pointer `path` with index `keyIndex`.
private final function bool IsKeyAcceptable(
    Collection      target,
    BaseJSONPointer path,
    int             keyIndex)
{
    local ArrayList arrayCollection;

    if (HashTable(target) != none)                  return true;
    arrayCollection = ArrayList(target);
    if (arrayCollection == none)                    return false;
    if (keyIndex >= path.GetLength())               return true;
    if (path.IsComponentArrayApplicable(keyIndex))  return true;

    return false;
}

//      Finds item inside `data` by using part of given JSON pointer as its own
//  pointer. Part is defined as pointer given by components with indices inside
//  `[from; to - 1]`.
//      `from` is an `out` argument that will return index of pointer's
//  component after that one used to obtained return value.
//      E.g. if pointer is "/a/b/c/d" and we returned value at "/a/b", then
//  `from` will contain index `2` of the component "c".
private final function AcediaObject ApplyPointer(
    AcediaObject    data,
    BaseJSONPointer pointer,
    out int         from,
    int             to)
{
    local int       nextNumericKey;
    local Text      nextKey;
    local ArrayList nextArray;
    local HashTable nextObject;

    if (from < 0 || from > to)      return none;
    if (data == none)               return none;
    if (pointer == none)            return none;
    if (to > pointer.GetLength())   return none;

    //      At each iteration in the `while` cycle below, `data` stores some
    //  reference to the next collection to "dig in" with our JSON pointer.
    //  This collection is normally obtained by `GetItem()` method on one of
    //  the iterations and, therefore, we own a reference to it that we must
    //  release.
    //      However, on the first iteration it is the same as passed argument
    //  and so we do not own it and cannot release it yet. We take ownership of
    //  it here yo hack around that issue.
    data.NewRef();
    while (from < to)
    {
        nextObject  = HashTable(data);
        nextArray   = ArrayList(data);
        //  Safe use `data` (and, therefore, both `nextObject` and `nextArray`)
        //  after this release, since `data` is either:
        //      1.  an argument and was added a reference before the loop
        //      2.  or is stored in another collection and, therefore, has
        //          another reference that way.
        //  Choice of `_.memory.Free()` instead of `self.FreeSelf()` is
        //  important here, since `data` can also be equal to `none`.
        //_.memory.Free(data);
        if (nextObject != none)
        {
            nextKey = pointer.GetComponent(from);
            if (!nextObject.HasKey(nextKey))
            {
                _.memory.Free(nextKey);
                return nextObject;
            }
            data.FreeSelf();
            data = nextObject.GetItem(nextKey);
            nextkey.FreeSelf();
        }
        else if (nextArray != none)
        {
            nextNumericKey = pointer.GetNumericComponent(from);
            if (nextNumericKey < 0 || nextNumericKey >= nextArray.GetLength()) {
                return nextArray;
            }
            data.FreeSelf();
            data = nextArray.GetItem(nextNumericKey);
        }
        else {
            //  Not a collection => we cannot "go in"
            return data;
        }
        from += 1;
    }
    return data;
}

//  Proper clean up of `PendingEdit`
private final function FreePendingWrite(PendingEdit edit)
{
    _.memory.Free(edit.location);
    _.memory.Free(edit.data);
}

defaultproperties
{
}