/**
 *  API that provides methods for creating/destroying and managing available
 *  databases.
 *      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 DBAPI extends AcediaObject;

var private const class<Database> localDBClass;

//      Store all already loaded databases to make sure we do not create two
//  different `LocalDatabaseInstance` that are trying to make changes
//  separately.
var private HashTable loadedLocalDatabases;

var private LoggerAPI.Definition infoLocalDatabaseCreated;
var private LoggerAPI.Definition infoLocalDatabaseDeleted;
var private LoggerAPI.Definition infoLocalDatabaseLoaded;

private final function CreateLocalDBMapIfMissing()
{
    if (loadedLocalDatabases == none) {
        loadedLocalDatabases = __().collections.EmptyHashTable();
    }
}

/**
 *  Loads database based on the link.
 *
 *  Links have the form of "<db_name>:" (or, optionally, "[<type>]<db_name>:"),
 *  followed by the JSON pointer (possibly empty one) to the object inside it.
 *  "<type>" can be either "local" or "remote" and is necessary only when both
 *  local and remote database have the same name (which should be avoided).
 *  "<db_name>" refers to the database that we are expected
 *  to load, it has to consist of numbers and latin letters only.
 *
 *  @param  databaseLink    Link from which to extract database's name.
 *  @return Database named "<db_name>" of type "<type>" from the `databaseLink`.
 */
public final function Database Load(BaseText databaseLink)
{
    local Parser        parser;
    local Database      result;
    local MutableText   databaseName;

    if (databaseLink == none) {
        return none;
    }
    parser = _.text.Parse(databaseLink);
    //  Only local DBs are supported for now!
    //  So just consume this prefix, if it's present.
    parser.Match(P("[local]")).Confirm();
    parser.R().MUntil(databaseName, _.text.GetCharacter(":")).MatchS(":");
    if (!parser.Ok())
    {
        parser.FreeSelf();
        return none;
    }
    result = LoadLocal(databaseName);
    parser.FreeSelf();
    databaseName.FreeSelf();
    return result;
}

/**
 *  Extracts `MutableJSONPointer` from the database path, given by `databaseLink`.
 *
 *  Links have the form of "<db_name>:" (or, optionally, "[<type>]<db_name>:"),
 *  followed by the JSON pointer (possibly empty one) to the object inside it.
 *  "<type>" can be either "local" or "remote" and is necessary only when both
 *  local and remote database have the same name (which should be avoided).
 *  "<db_name>" refers to the database that we are expected
 *  to load, it has to consist of numbers and latin letters only.
 *  This method returns `MutableJSONPointer` that comes after type-name pair.
 *
 *  @param  Link from which to extract `MutableJSONPointer`.
 *  @return `MutableJSONPointer` from the database link.
 *      Guaranteed to not be `none` if provided argument `databaseLink`
 *      is not `none`.
 */
public final function MutableJsonPointer GetMutablePointer(BaseText databaseLink)
{
    local int                   slashIndex;
    local Text                  textPointer;
    local MutableJsonPointer    result;

    if (databaseLink == none) {
        return none;
    }
    slashIndex = databaseLink.IndexOf(P(":"));
    if (slashIndex < 0) {
        return MutableJsonPointer(_.memory.Allocate(class'MutableJsonPointer'));
    }
    textPointer = databaseLink.Copy(slashIndex + 1);
    result = _.json.MutablePointer(textPointer);
    textPointer.FreeSelf();
    return result;
}

/**
 *  Extracts `JSONPointer` from the database path, given by `databaseLink`.
 *
 *  Links have the form of "<db_name>:" (or, optionally, "[<type>]<db_name>:"),
 *  followed by the JSON pointer (possibly empty one) to the object inside it.
 *  "<type>" can be either "local" or "remote" and is necessary only when both
 *  local and remote database have the same name (which should be avoided).
 *  "<db_name>" refers to the database that we are expected
 *  to load, it has to consist of numbers and latin letters only.
 *  This method returns `JSONPointer` that comes after type-name pair.
 *
 *  @param  Link from which to extract `JSONPointer`.
 *  @return `JSONPointer` from the database link.
 *      Guaranteed to not be `none` if provided argument `databaseLink`
 *      is not `none`.
 */
public final function JSONPointer GetPointer(BaseText databaseLink)
{
    local int           slashIndex;
    local Text          textPointer;
    local JSONPointer   result;

    if (databaseLink == none) {
        return none;
    }
    slashIndex = databaseLink.IndexOf(P(":"));
    if (slashIndex < 0) {
        return JSONPointer(_.memory.Allocate(class'JSONPointer'));
    }
    textPointer = databaseLink.Copy(slashIndex + 1);
    result = _.json.Pointer(textPointer);
    textPointer.FreeSelf();
    return result;
}

/**
 *  Opens a new `DBConnection` to the data referred to by the database link.
 *
 *  Opened `DBConnection` doesn't automatically start a connection, so you
 *  need to call its `Connect()` method.
 *
 *  @param  databaseLink    Database link to the data we want to connect to.
 *  @return Initialized `DBConnection` in case given link is valid and `none`
 *      otherwise.
 */
public final function DBConnection OpenConnection(BaseText databaseLink)
{
    local DBConnection  result;
    local Parser        parser;
    local Database      databaseToConnect;
    local JSONPointer   locationToConnect;
    local MutableText   databaseName, textPointer;

    if (databaseLink == none) {
        return none;
    }
    parser = _.text.Parse(databaseLink);
    //  Only local DBs are supported for now!
    //  So just consume this prefix, if it's present.
    parser.Match(P("[local]")).Confirm();
    textPointer = parser
        .R()
        .MUntil(databaseName, _.text.GetCharacter(":"))
        .MatchS(":")
        .GetRemainderM();
    if (parser.Ok())
    {
        databaseToConnect = LoadLocal(databaseName);
        locationToConnect = _.json.Pointer(textPointer);
        result = DBConnection(_.memory.Allocate(class'DBConnection'));
        result.Initialize(databaseToConnect, locationToConnect);
        _.memory.Free(databaseToConnect);
        _.memory.Free(locationToConnect);
    }
    parser.FreeSelf();
    _.memory.Free(databaseName);
    _.memory.Free(textPointer);
    if (result != none && !result.IsInitialized())
    {
        result.FreeSelf();
        result = none;
    }
    return result;
}

/**
 *  Creates new local database with name `databaseName`.
 *
 *  This method will fail if:
 *      1. `databaseName` is `none` or empty;
 *      2. Local database with name `databaseName` already exists.
 *
 *  @param  databaseName    Name for the new database.
 *  @return Reference to created database. Returns `none` iff method failed.
 */
public final function LocalDatabaseInstance NewLocal(BaseText databaseName)
{
    local DBRecord              rootRecord;
    local Text                  rootRecordName;
    local Text                  databaseNameCopy;
    local LocalDatabase         newConfig;
    local LocalDatabaseInstance newLocalDBInstance;

    CreateLocalDBMapIfMissing();
    if (databaseName == none)                       return none;
    if (!databaseName.IsValidName())                return none;
    newConfig = class'LocalDatabase'.static.Load(databaseName);
    if (newConfig == none)                          return none;
    if (newConfig.HasDefinedRoot())                 return none;
    if (loadedLocalDatabases.HasKey(databaseName))  return none;

    newLocalDBInstance = LocalDatabaseInstance(_.memory.Allocate(localDBClass));
    databaseNameCopy = databaseName.Copy();
    loadedLocalDatabases.SetItem(databaseNameCopy, newLocalDBInstance);
    rootRecord = class'DBRecord'.static.NewRecord(databaseName);
    rootRecordName = _.text.FromString(string(rootRecord.name));
    newConfig.SetRootName(rootRecordName);
    newConfig.Save();
    newLocalDBInstance.Initialize(newConfig, rootRecord);
    _.logger.Auto(infoLocalDatabaseCreated).Arg(databaseNameCopy);
    _.memory.Free(rootRecordName);
    return newLocalDBInstance;
}

/**
 *  Loads and returns local database with the name `databaseName`.
 *
 *  If specified database is already loaded - simply returns it's reference
 *  (consequent calls to `LoadLocal()` will keep returning the same reference,
 *  unless database is deleted).
 *
 *  @param  databaseName    Name of the database to load.
 *  @return Loaded local database. `none` if it does not exist.
 */
public final function LocalDatabaseInstance LoadLocal(BaseText databaseName)
{
    local DBRecord              rootRecord;
    local Text                  rootRecordName;
    local LocalDatabase         newConfig;
    local LocalDatabaseInstance newLocalDBInstance, result;
    local Text                  dbKey;

    if (databaseName == none) {
        return none;
    }
    CreateLocalDBMapIfMissing();
    dbKey = databaseName.Copy();
    if (loadedLocalDatabases.HasKey(dbKey))
    {
        result = LocalDatabaseInstance(loadedLocalDatabases.GetItem(dbKey));
        _.memory.Free(dbKey);
        return result;
    }
    //  No need to check `dbKey` for being valid,
    //  since `Load()` will just return `none` if it is not.
    newConfig = class'LocalDatabase'.static.Load(dbKey);
    if (newConfig == none) {
        _.memory.Free(dbKey);
        return none;
    }
    if (!newConfig.HasDefinedRoot() && !newConfig.ShouldCreateIfMissing()) {
        _.memory.Free(dbKey);
        return none;
    }
    newLocalDBInstance = LocalDatabaseInstance(_.memory.Allocate(localDBClass));
    loadedLocalDatabases.SetItem(dbKey, newLocalDBInstance);
    dbKey.FreeSelf();
    if (newConfig.HasDefinedRoot())
    {
        rootRecordName = newConfig.GetRootName();
        rootRecord = class'DBRecord'.static.LoadRecord(rootRecordName, dbKey);
    }
    else
    {
        rootRecord = class'DBRecord'.static.NewRecord(dbKey);
        rootRecordName = _.text.FromString(string(rootRecord.name));
        newConfig.SetRootName(rootRecordName);
        newConfig.Save();
    }
    newLocalDBInstance.Initialize(newConfig, rootRecord);
    _.logger.Auto(infoLocalDatabaseLoaded).Arg(dbKey);
    _.memory.Free(rootRecordName);
    return newLocalDBInstance;
}

/**
 *  Checks if local database with the name `databaseName` already exists.
 *
 *  @param  databaseName    Name of the database to check.
 *  @return `true` if database with specified name exists and `false` otherwise.
 */
public final function bool ExistsLocal(BaseText databaseName)
{
    local bool                  result;
    local LocalDatabaseInstance instance;

    instance = LoadLocal(databaseName);
    result = (instance != none);
    _.memory.Free(instance);
    return result;
}

/**
 *  Deletes local database with name `databaseName`.
 *
 *  @param  databaseName    Name of the database to delete.
 *  @return `true` if database with specified name existed and was deleted and
 *      `false` otherwise.
 */
public final function bool DeleteLocal(BaseText databaseName)
{
    local LocalDatabase         localDatabaseConfig;
    local LocalDatabaseInstance localDatabase;
    local HashTable.Entry       dbEntry;

    if (databaseName == none) {
        return false;
    }
    CreateLocalDBMapIfMissing();
    //  To delete database we first need to load it
    localDatabase = LoadLocal(databaseName);
    if (localDatabase != none)
    {
        localDatabaseConfig = localDatabase.GetConfig();
        localDatabase.WriteToDisk();
        _.memory.Free(localDatabase);
    }
    dbEntry = loadedLocalDatabases.TakeEntry(databaseName);
    //  Delete `LocalDatabaseInstance` before erasing the package,
    //  to allow it to clean up safely
    _.memory.Free(dbEntry.key);
    _.memory.Free(dbEntry.value);
    if (localDatabaseConfig != none)
    {
        EraseAllPackageData(localDatabaseConfig.GetPackageName());
        localDatabaseConfig.DeleteSelf();
        _.logger.Auto(infoLocalDatabaseDeleted).Arg(databaseName.Copy());
        return true;
    }
    return false;
}

private function EraseAllPackageData(BaseText packageToErase)
{
    local int               i;
    local string            packageName;
    local GameInfo          game;
    local DBRecord          nextRecord;
    local array<DBRecord>   allRecords;

    packageName = _.text.IntoString(packageToErase);
    if (packageName == "") {
        return;
    }
    game = __level().unreal_api().GetGameType();
    game.DeletePackage(packageName);
    //  Delete any leftover objects. This has to be done *after*
    //  `DeletePackage()` call, otherwise removed garbage can reappear.
    //  No clear idea why it works this way.
    foreach game.AllDataObjects(class'DBRecord', nextRecord, packageName) {
        allRecords[allRecords.length] = nextRecord;
    }
    for (i = 0; i < allRecords.length; i += 1)
    {
        game.DeleteDataObject(  class'DBRecord', string(allRecords[i].name),
                                packageName);
    }
}

/**
 *  Returns array of names of all available local databases.
 *
 *  @return List of names of all local databases.
 */
public final function array<Text> ListLocal()
{
    local int           i;
    local array<Text>   dbNames;
    local array<string> dbNamesAsStrings;

    dbNamesAsStrings = GetPerObjectNames(   "AcediaDB",
                                            string(class'LocalDatabase'.name),
                                            MaxInt);
    for (i = 0; i < dbNamesAsStrings.length; i += 1) {
        dbNames[dbNames.length] = _.text.FromString(dbNamesAsStrings[i]);
    }
    return dbNames;
}

defaultproperties
{
    localDBClass = class'LocalDatabaseInstance'
    infoLocalDatabaseCreated = (l=LOG_Info,m="Local database \"%1\" was created.")
    infoLocalDatabaseLoaded = (l=LOG_Info,m="Local database \"%1\" was loaded.")
    infoLocalDatabaseDeleted = (l=LOG_Info,m="Local database \"%1\" was deleted.")
}