/**
 *  Auxiliary object for simplifying working with databases.
 *      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 DBConnection extends AcediaObject
    dependson(Database)
    dependson(DBCache);

/**
 *  # `DBConnection`
 *
 *      Auxiliary object for simplifying working with databases.
 *  `Database` class has a rather simple interface and there are several issues
 *  that constantly arise when trying to use it:
 *
 *      1.  If one tries to read/write data from/to specific location in
 *          the database, then `JSONPointer` has to be kept in addition to
 *          the `Database` reference at all times;
 *      2.  One has to perform initial checks about whether database can even be
 *          connected to, if at desired location there is a proper data
 *          structure, etc.. If one also wants to start using data before
 *          database's response or after its failure, then the same work of
 *          duplication that data locally must be performed.
 *      3.  Instead of immediate operations, database operations are delayed and
 *          user has to handle their results asynchronously in separate methods.
 *
 *      `DBConnection` takes care of these issues by providing you synchronous
 *  methods for accessing cached version of the data at the given location that
 *  is duplicated to the database as soon as possible (and even if its no longer
 *  possible in the case of a failure).
 *      `DBConnection` makes immediate changes on the local cache and reports
 *  about possible failures with database later through signals `OnEditResult()`
 *  (reports about success of writing operations) and `OnStateChanged()`
 *  (reports about state changes of connected database, including complete
 *  failures).
 *      The only reading of database's values occurs at the moment of connecting
 *  to it, after that all the data is read from the local cache.
 *      Possible `DBConnection` states include:
 *
 *      *   `DBCS_Idle` - database was created, but not yet connected;
 *      *   `DBCS_Connecting` - `Connect()` method was successfully called, but
 *          its result is still unknown;
 *      *   `DBCS_Connected` - database is connected and properly working;
 *      *   `DBCS_Disconnected` - database was manually disconnected and now
 *          operates solely on the local cache. Once disconnected `DBConnection`
 *          cannot be reconnected - create a new one instead.
 *      *   `DBCS_FaultyDatabase` - database is somehow faulty. Precise reason
 *          can be found out with `GetError()` method:
 *
 *          *   `FDE_None` - no error has yet occurred;
 *          *   `FDE_CannotReadRootData` - root data couldn't be read from
 *              the database, most likely because of the invalid `JSONPointer`
 *              for the root value;
 *          *   `FDE_UnexpectedRootData` - root data was read from the database,
 *              but has an unexpected format. `DBConnection` expects either
 *              JSON object or array (can be specified which one) and this error
 *              occurs if its not found at specified database's location;
 *          *   `FDE_Unknown` - database returned `DBR_InvalidDatabase` result
 *              for one of the queries. This is likely to happen when database
 *              is damaged. More precise details depend on the implementation,
 *              but result boils down to database being unusable.
 *
 *  ## Usage
 *
 *  Usage is straightforward:
 *
 *      1.  Initialize with appropriate database by calling `Initialize()`;
 *      2.  Start connecting to it by calling `Connect()`;
 *      3.  Use `ReadDataByJSON()`/`WriteDataByJSON()` to read/write into
 *          the connected database;
 *
 *  You can use it transparently even if database connection fails, but if you
 *  need to handle such failure - connect to the `OnStateChanged()` signal for
 *  tracking `DBCS_FaultyDatabase` state and to `OnEditResult()` for tracking
 *  success of writing operations.
 *
 *  ## Implementation
 *
 *      The brunt of work is done by `DBCache` and most of the logic in this
 *  class is for tracking state of the connection to the database and then
 *  reporting these changes through its own signals.
 *      The most notable hidden functionality is tracking requests by ID -
 *  `DBConnection` makes each request with unique ID and then stores them inside
 *  `requestIDs` (and in case of write requests - along with corresponding
 *  `JSONPointer` inside `queuedPointers`). This is necessary because:
 *
 *      1.  Even if `DBConnection` gets reallocated - as far as UnrealScript is
 *          concerned it is still the same object, so the responses we no longer
 *          care about will still arrive. Keeping track of the IDs that interest
 *          us inside `requestIDs` allows us to filter out responses we no
 *          longer care about;
 *      2.  Tracking corresponding (to the IDs) `queuedPointers` also allow us
 *          to know responses to which writing requests we've received.
 *
 *  ## Remarks
 *
 *  Currently `DBConnection` doesn't support important feature of *incrementing*
 *  data that allows several sources to safely change the same value
 *  asynchronously. We're skipping on it right now to save time as its not
 *  really currently needed, however it will be added in the future.
 */

enum DBConnectionState
{
    //  `DBConnection` was created, but didn't yet attempt to connect
    //  to database
    DBCS_Idle,
    //  `DBConnection` is currently connecting
    DBCS_Connecting,
    //  `DBConnection` has already connected without errors
    DBCS_Connected,
    //  `DBConnection` was manually disconnected
    DBCS_Disconnected,
    //  `DBConnection` was disconnected because of the database error,
    //  @see `FaultyDatabaseError` for more.
    DBCS_FaultyDatabase
};
//  Current connection state
var private DBConnectionState currentState;

enum FaultyDatabaseError
{
    //  No error has occurred yet
    FDE_None,
    //  Root data isn't available
    FDE_CannotReadRootData,
    //  Root data has incorrect format
    FDE_UnexpectedRootData,
    //  Some internal error in database has occurred
    FDE_Unknown
};
//  Reason for why current state is in `DBCS_FaultyDatabase` state;
//  `FDE_None` if `DBConnection` is in any other state.
var private FaultyDatabaseError dbFailureReason;
//      Keeps track whether root value read from the database was of the correct
//  type. Only relevant after database tried connecting (i.e. it is in states
//  `DBCS_Connected`, `DBCS_Disconnected` or `DBCS_FaultyDatabase`).
//      This variable helps us determine whether error should be
//  `FDE_CannotReadRootData` or `FDE_UnexpectedRootData`.
var private bool                rootIsOfExpectedType;

//  `Database` + `JSONPointer` combo that point at the data we want to
//  connect to
var private Database    dbInstance;
var private JSONPointer rootPointer;
//  Local, cached version of that data
var private DBCache     localCache;

//  This is basically an array of (`int`, `JSONPointer`) pairs for tracking
//  database requests of interest
var private array<int>          requestIDs;
var private array<JSONPointer>  queuedPointers;
//  Next usable ID. `DBConnection` is expected to always use unique IDs.
var private int                 nextRequestID;


var private DBConnection_StateChanged_Signal    onStateChangedSignal;
var private DBConnection_EditResult_Signal      onEditResultSignal;

var private LoggerAPI.Definition errDoubleInitialization;

/**
 *  Signal that will be emitted whenever `DBConnection` changes state.
 *  List of the available states:
 *
 *      *   `DBCS_Idle` - database was created, but not yet connected;
 *      *   `DBCS_Connecting` - `Connect()` method was successfully called, but
 *          its result is still unknown;
 *      *   `DBCS_Connected` - database is connected and properly working;
 *      *   `DBCS_Disconnected` - database was manually disconnected and now
 *          operates solely on the local cache. Once disconnected `DBConnection`
 *          cannot be reconnected - create a new one instead.
 *      *   `DBCS_FaultyDatabase` - database is somehow faulty. Precise reason
 *          can be found out with `GetError()` method.
 *
 *  This method *is not* called when `DBConnection` is deallocated.
 *
 *  [Signature]
 *  void <slot>(
 *      DBConnection        instance,
 *      DBConnectionState   oldState,
 *      DBConnectionState   newState)
 *
 *  @param  instance    Instance of the `DBConnection` that has changed state.
 *  @param  oldState    State it was previously in.
 *  @param  oldState    New state.
 */
/* SIGNAL */
public final function DBConnection_StateChanged_Slot OnStateChanged(
    AcediaObject receiver)
{
    return DBConnection_StateChanged_Slot(onStateChangedSignal
        .NewSlot(receiver));
}

/**
 *  Signal that will be emitted whenever `DBConnection` receives response from
 *  connected database about success of writing operation.
 *
 *  Responses to old requests can still be received even if database got
 *  disconnected.
 *
 *  Any emissions of this signal when the database's state is `DBCS_Connecting`
 *  correspond to reapplying edits made prior connection was established.
 *
 *  [Signature]
 *  void <slot>(JSONPointer editLocation, bool isSuccessful)
 *
 *  @param  editLocation    Location of the writing operation this is
 *      a response to.
 *  @param  isSuccessful    Whether writing operation ended in the success.
 */
/* SIGNAL */
public final function DBConnection_EditResult_Slot OnEditResult(
    AcediaObject receiver)
{
    return DBConnection_EditResult_Slot(onEditResultSignal.NewSlot(receiver));
}

protected function Constructor()
{
    localCache = DBCache(_.memory.Allocate(class'DBCache'));
    onStateChangedSignal = DBConnection_StateChanged_Signal(
        _.memory.Allocate(class'DBConnection_StateChanged_Signal'));
    onEditResultSignal = DBConnection_EditResult_Signal(
        _.memory.Allocate(class'DBConnection_EditResult_Signal'));
}

protected function Finalizer()
{
    rootIsOfExpectedType = false;
    currentState = DBCS_Idle;
    _.memory.Free(dbInstance);
    _.memory.Free(rootPointer);
    _.memory.Free(localCache);
    dbInstance  = none;
    rootPointer = none;
    localCache  = none;
    _.memory.FreeMany(queuedPointers);
    queuedPointers.length = 0;
    requestIDs.length = 0;
    //  Free signals
    _.memory.Free(onStateChangedSignal);
    _.memory.Free(onEditResultSignal);
    onStateChangedSignal    = none;
    onEditResultSignal      = none;
}

/**
 *  Initializes `DBConnection` with database and location to which it must be
 *  connected.
 *
 *  For the initialization to be successful `DBConnection` must not yet be
 *  initialized and `initDatabase` be not `none`.
 *
 *  To check whether caller `DBConnection` is initialized
 *  @see `IsInitialized()`.
 *
 *  @param  initDatabase    Database with data we want to connect to.
 *  @param  initRootPointer Location of said data in the given database.
 *      If `none` is specified, uses root object of the database.
 *  @return `true` if initialization was successful and `false` otherwise.
 */
public final function bool Initialize(
    Database                    initDatabase,
    optional BaseJSONPointer    initRootPointer)
{
    if (IsInitialized())                return false;
    if (initDatabase == none)           return false;
    if (!initDatabase.IsAllocated())    return false;

    dbInstance = initDatabase;
    dbInstance.NewRef();
    if (initRootPointer != none) {
        rootPointer = initRootPointer.Copy();
    }
    else {
        rootPointer = _.json.Pointer();
    }
    return true;
}

/**
 *  Reads data from the `DBConnection` at the location defined by the given
 *  `JSONPointer`.
 *
 *  If data was initialized with non-empty location for the root data, then
 *  actual returned data's location in the database is defined by appending
 *  given `pointer` to that root pointer.
 *
 *  Data is actually always read from the local cache and, therefore, we can
 *  read data we've written via `DBConnection` even without actually connecting
 *  to the database.
 *
 *  @param  pointer Location from which to read the data.
 *  @return Data recorded for the given `JSONPointer`. `none` if it is missing.
 */
public final function AcediaObject ReadDataByJSON(BaseJSONPointer pointer)
{
    return localCache.Read(pointer);
}

/**
 *  Writes given data into the `DBConnection` at the location defined by
 *  the given `JSONPointer`.
 *
 *  If data was initialized with non-empty location for the root data, then
 *  actual location for writing data in the database is defined by appending
 *  given `pointer` to that root pointer.
 *
 *      Data is actually always also written into the local cache, even when
 *  there is no connection to the database. Once connection is made - all valid
 *  changes will be duplicated into it.
 *      Success of failure of actually making changes into the database can be
 *  tracked with `OnEditResult()` signal.
 *
 *      This operation also returns immediate indication of whether it has
 *  failed *locally*. This can happen when trying to perform operation
 *  impossible for the local cache. For example, we cannot write any data at
 *  location "/a/b/c" for the JSON object "{"a":45.6}".
 *      If operation ended in failure locally, then change to database won't
 *  even be attempted.
 *
 *  @param  pointer Location into which to write the data.
 *  @param  data    Data to write into the connection.
 *  @return `true` on success and `false` on failure. `true` is required for
 *      the writing database request to be made.
 */
public final function bool WriteDataByJSON(
    BaseJSONPointer pointer,
    AcediaObject    data)
{
    if (pointer == none) {
        return false;
    }
    if (localCache.Write(pointer, data))
    {
        ModifyDataInDatabase(pointer, data, false);
        return true;
    }
    return false;
}

/**
 *  Increments given data into the `DBConnection` at the location defined by
 *  the given `JSONPointer`.
 *
 *  If data was initialized with non-empty location for the root data, then
 *  actual location for incrementing data in the database is defined by
 *  appending given `pointer` to that root pointer.
 *
 *      Data is actually always also incremented into the local cache, even when
 *  there is no connection to the database. Once connection is made - all valid
 *  changes will be duplicated into it.
 *      Success of failure of actually making changes into the database can be
 *  tracked with `OnEditResult()` signal.
 *
 *      This operation also returns immediate indication of whether it has
 *  failed *locally*. This can happen when trying to perform operation
 *  impossible for the local cache. For example, we cannot increment any data at
 *  location "/a/b/c" for the JSON object "{"a":45.6}".
 *      If operation ended in failure locally, then change to database won't
 *  even be attempted.
 *
 *  @param  pointer Location at which to increment the data.
 *  @param  data    Data with which to increment value inside the connection.
 *  @return `true` on success and `false` on failure. `true` is required for
 *      the incrementing database request to be made.
 */
public final function bool IncrementDataByJSON(
    BaseJSONPointer pointer,
    AcediaObject    data)
{
    if (pointer == none) {
        return false;
    }
    if (localCache.Increment(pointer, data))
    {
        ModifyDataInDatabase(pointer, data, true);
        return true;
    }
    return false;
}

/**
 *  Removes data from the `DBConnection` at the location defined by the given
 *  `JSONPointer`.
 *
 *  If data was initialized with non-empty location for the root data, then
 *  actual location at which to remove data in the database is defined by
 *  appending given `pointer` to that root pointer.
 *
 *      Data is actually always also removed from the local cache, even when
 *  there is no connection to the database. Once connection is made - all valid
 *  changes will be duplicated into it.
 *      Success of failure of actually making changes into the database can be
 *  tracked with `OnEditResult()` signal.
 *
 *      This operation also returns immediate indication of whether it has
 *  failed *locally*.
 *      If operation ended in failure locally, then change to database won't
 *  even be attempted.
 *
 *  @param  pointer Location at which to remove data.
 *  @return `true` on success and `false` on failure. `true` is required for
 *      the removal database request to be made.
 */
public final function bool RemoveDataByJSON(BaseJSONPointer pointer)
{
    if (pointer == none) {
        return false;
    }
    if (localCache.Remove(pointer))
    {
        RemoveDataInDatabase(pointer);
        return true;
    }
    return false;
}

private final function ModifyDataInDatabase(
    BaseJSONPointer pointer,
    AcediaObject    data,
    bool            increment)
{
    local MutableJSONPointer dataPointer;

    if (currentState != DBCS_Connected) {
        return;
    }
    dataPointer = rootPointer.MutableCopy();
    dataPointer.Append(pointer);
    //  `dataPointer` is consumed by `RegisterNextRequestID()` method
    if (increment)
    {
        dbInstance
            .IncrementData(
                dataPointer,
                data,
                RegisterNextRequestID(/*take*/ dataPointer.Copy()))
            .connect = EditDataHandler;
        _.memory.Free(dataPointer);
    }
    else
    {
        dbInstance
            .WriteData(dataPointer, data, RegisterNextRequestID(/*take*/ dataPointer.Copy()))
            .connect = EditDataHandler;
        _.memory.Free(dataPointer);
    }
}

private final function RemoveDataInDatabase(BaseJSONPointer pointer)
{
    local MutableJSONPointer dataPointer;

    if (currentState != DBCS_Connected) {
        return;
    }
    dataPointer = rootPointer.MutableCopy();
    dataPointer.Append(pointer);
    //  `dataPointer` is consumed by `RegisterNextRequestID()` method
    dbInstance
        .RemoveData(dataPointer, RegisterNextRequestID(/*take*/ dataPointer.Copy()))
        .connect = EditDataHandler;
    _.memory.Free(dataPointer);
}

/**
 *  Checks caller `DBConnection` was successfully initialized.
 *
 *  @return `true` if caller `DBConnection` was initialized and `false`
 *      otherwise.
 */
public final function bool IsInitialized()
{
    return (dbInstance != none);
}

/**
 *  Returns current state of the connection of `DBConnection` to the database
 *  it was initialized with.
 *
 *  @see `OnStateChanged()` for more information about connection states.
 *  @return Current connection state.
 */
public final function DBConnectionState GetConnectionState()
{
    return currentState;
}

/**
 *  Checks whether caller `DBConnection` is currently connected without errors
 *  to the database it was initialized with.
 *
 *  @return `true` if caller `DBConnection` is connected to the database and
 *      `false` otherwise.
 */
public final function bool IsConnected()
{
    return (currentState == DBCS_Connected);
}

/**
 *  Checks whether an error has occurred with connection to the database.
 *
 *      `DBConnection` can get disconnected from database manually and without
 *  any errors, so, if you simply want to check whether connection exists,
 *  @see `IsConnected()` or @see `GetConnectionState()`.
 *      To obtain more detailed information @see `GetError()`.
 *
 *  @return `true` if there were no error thus far and `false` otherwise.
 */
public final function bool IsOk()
{
    return (dbFailureReason == FDE_None);
}

/**
 *  Returns error that has occurred during connection.
 *
 *  @return Error that has occurred during connection to the database,
 *      `FDE_None` if there was no errors.
 */
public final function FaultyDatabaseError GetError()
{
    return dbFailureReason;
}

private final function ChangeState(DBConnectionState newState)
{
    local DBConnectionState oldState;

    oldState = currentState;
    currentState = newState;
    onStateChangedSignal.Emit(self, oldState, newState);
}

/**
 *  Attempts connection to the database caller `DBConnection` was initialized
 *  with. Result isn't immediate and can be tracked with `OnStateChanged()`
 *  signal.
 *
 *  Connection checks whether data by the initialization address can be read and
 *  has proper type (by default JSON object, but JSON array can be used
 *  instead).
 *
 *  Whether connection is successfully established isn't known at the moment
 *  this function returns. User `OnStateChanged()` to track that.
 *
 *  @param  expectArray Set this to `true` if the expected root value is
 *      JSON array.
 */
public final function Connect(optional bool expectArray)
{
    local Collection incrementObject;

    if (!IsInitialized())           return;
    if (currentState != DBCS_Idle)  return;

    if (expectArray) {
        incrementObject = _.collections.EmptyArrayList();
    }
    else {
        incrementObject = _.collections.EmptyHashTable();
    }
    dbInstance.IncrementData(
        rootPointer,
        incrementObject,
        RegisterNextRequestID()).connect = IncrementCheckHandler;
    incrementObject.FreeSelf();
    //  Copy of the `rootPointer` is consumed by `RegisterNextRequestID()`
    //  method
    dbInstance.ReadData(rootPointer,, RegisterNextRequestID(rootPointer.Copy()))
        .connect = InitialLoadingHandler;
    ChangeState(DBCS_Connecting);
}

/**
 *  Disconnects `DBConnection` from its database, preventing its further
 *  updates.
 *
 *  Database can only be disconnected if connection was at least initialized
 *  (state isn't `DBCS_Idle`) and no error has yet occurred (state isn't
 *  `DBCS_FaultyDatabase`).
 *
 *  @return `true` if `DBConnection` was disconnected from the database and
 *      `false` otherwise (including if it already was disconnected).
 */
public final function bool Disconnect()
{
    if (    currentState != DBCS_FaultyDatabase
        &&  currentState != DBCS_Idle
        &&  currentState != DBCS_Disconnected)
    {
        ChangeState(DBCS_Disconnected);
        return true;
    }
    return false;
}

private final function int RegisterNextRequestID(
    optional /*take*/ JSONPointer relativePointer)
{
    if (relativePointer != none) {
        queuedPointers[queuedPointers.length] = relativePointer;
    }
    else {
        queuedPointers[queuedPointers.length] = _.json.Pointer();
    }
    requestIDs[requestIDs.length] = nextRequestID;
    nextRequestID += 1;
    return (nextRequestID - 1);
}

private final function JSONPointer FetchRequestPointer(int requestID)
{
    local int           i;
    local JSONPointer   result;

    while (i < requestIDs.length)
    {
        if (requestIDs[i] < requestID)
        {
            //  We receive all requests in order, so if `requestID` is higher
            //  than IDs of some other requests - it means that they are older,
            //  lost requests
            _.memory.Free(queuedPointers[i]);
            queuedPointers.Remove(i, 1);
            requestIDs.Remove(i, 1);
        }
        if (requestIDs[i] == requestID)
        {
            result = queuedPointers[i];
            queuedPointers.Remove(i, 1);
            requestIDs.Remove(i, 1);
            return result;
        }
        i += 1;
    }
    return none;
}

private final function bool FetchIfRequestStillValid(int requestID)
{
    local JSONPointer result;

    result = FetchRequestPointer(requestID);
    if (result != none)
    {
        _.memory.Free(result);
        return true;
    }
    return false;
}

private final function IncrementCheckHandler(
    Database.DBQueryResult  result,
    Database                source,
    int                     requestID)
{
    if (!FetchIfRequestStillValid(requestID)) {
        return;
    }
    //  If we could successfully increment value with appropriate JSON value,
    //  then its type is correct
    rootIsOfExpectedType = (result == DBR_Success);
}

private final function InitialLoadingHandler(
    Database.DBQueryResult  result,
    /*take*/ AcediaObject   data,
    Database                source,
    int                     requestID)
{
    local int                           i;
    local array<DBCache.PendingEdit>    completedEdits;

    if (!FetchIfRequestStillValid(requestID))
    {
        _.memory.Free(data);
        return;
    }
    if (HandleInitializationError(result))
    {
        _.memory.Free(data);
        return;
    }
    completedEdits = localCache.SetRealData(data);
    for (i = 0; i < completedEdits.length; i += 1)
    {
        if (completedEdits[i].successful)
        {
            if (completedEdits[i].type == DBCET_Remove) {
                RemoveDataInDatabase(completedEdits[i].location);
            }
            else
            {
                ModifyDataInDatabase(
                    completedEdits[i].location,
                    completedEdits[i].data,
                    completedEdits[i].type == DBCET_Increment);
            }
        }
        else {
            onEditResultSignal.Emit(completedEdits[i].location, false);
        }
        _.memory.Free(completedEdits[i].location);
        _.memory.Free(completedEdits[i].data);
    }
    _.memory.Free(data);
    ChangeState(DBCS_Connected);
}

//  Return `true` if further initialization must be stopped.
private final function bool HandleInitializationError(
    Database.DBQueryResult result)
{
    //  Get disconnected before even response has even arrived
    if (currentState == DBCS_Disconnected) {
        return true;
    }
    if (currentState == DBCS_Connected)
    {
        _.logger.Auto(errDoubleInitialization).Arg(rootPointer.ToText());
        return true;
    }
    if (result == DBR_InvalidDatabase)
    {
        dbFailureReason = FDE_Unknown;
        ChangeState(DBCS_FaultyDatabase);
        return true;
    }
    if (result != DBR_Success)
    {
        dbFailureReason = FDE_CannotReadRootData;
        ChangeState(DBCS_FaultyDatabase);
        return true;
    }
    if (!rootIsOfExpectedType)
    {
        dbFailureReason = FDE_UnexpectedRootData;
        ChangeState(DBCS_FaultyDatabase);
        return true;
    }
    return false;
}

private final function EditDataHandler(
    Database.DBQueryResult  result,
    Database                source,
    int                     requestID)
{
    local JSONPointer relatedPointer;

    relatedPointer = FetchRequestPointer(requestID);
    if (relatedPointer == none) {
        return;
    }
    if (result == DBR_InvalidDatabase)
    {
        dbFailureReason = FDE_Unknown;
        ChangeState(DBCS_FaultyDatabase);
        relatedPointer.FreeSelf();
        return;
    }
    if (result == DBR_Success) {
        onEditResultSignal.Emit(relatedPointer, true);
    }
    else {
        onEditResultSignal.Emit(relatedPointer, false);
    }
    relatedPointer.FreeSelf();
}

defaultproperties
{
    errDoubleInitialization = (l=LOG_Error,m="`DBConnection` connected to \"%1\" was double-initialized. This SHOULD NOT happen. Please report this bug.")
}