/**
 * Author: dkanus
 * Home repo: https://www.insultplayers.ru/git/AcediaFramework/AcediaCore
 * License: GPL
 * 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 CmdItemsTool extends AcediaObject
    dependson(CommandAPI)
    abstract;

//! This is a base class for auxiliary objects that will be used for storing
//! named [`Command`] instances and [`Voting`] classes: they both have in common
//! the need to remember who was authorized to use them (i.e. which user group)
//! and with what permissions (i.e. name of the config that contains appropriate
//! permissions).
//!
//! Aside from trivial accessors to its data, it also provides a way to resolve
//! the best permissions available to the user by finding the most priviledged
//! group he belongs to.
//!
//! NOTE: child classes must implement `MakeCard()` method and can override
//! `DiscardCard()` method to catch events of removing items from storage.

/// Allows to specify a base class requirement for this tool - only classes
/// that were derived from it can be stored inside.
var protected const class<AcediaObject> ruleBaseClass;

/// Names of user groups that can decide permissions for items,
/// in order of importance: from most significant to the least significant.
/// This is used for resolving the best permissions for each user.
var private array<Text> permissionGroupOrder;

/// Maps item names to their [`ItemCards`] with information about which groups
/// are authorized to use this particular item.
var private HashTable registeredCards;

var LoggerAPI.Definition errItemInvalidName;
var LoggerAPI.Definition errItemDuplicate;

protected function Constructor() {
    registeredCards = _.collections.EmptyHashTable();
}

protected function Finalizer() {
    _.memory.Free(registeredCards);
    _.memory.FreeMany(permissionGroupOrder);
    registeredCards = none;
    permissionGroupOrder.length = 0;
}

/// Registers given item class under the specified (case-insensitive) name.
///
/// If name parameter is omitted (specified as `none`) or is an invalid name
/// (according to [`BaseText::IsValidName()`] method), then item class will not
/// be registered.
///
/// Returns `true` if item was successfully registered and `false` otherwise`.
///
/// # Errors
///
/// If provided name that is invalid or already taken by a different item -
/// a warning will be logged and item class won't be registered.
public function bool AddItemClass(class<AcediaObject> itemClass, BaseText itemName) {
    local Text itemKey;
    local ItemCard newCard, existingCard;

    if (itemClass == none)          return false;
    if (itemName == none)           return false;
    if (registeredCards == none)    return false;

    if (ruleBaseClass == none || !ClassIsChildOf(itemClass, ruleBaseClass)) {
        return false;
    }
    // The item name is transformed into lowercase, immutable value.
    // This facilitates the use of item names as keys in a [`HashTable`],
    // enabling case-insensitive matching.
    itemKey = itemName.LowerCopy();
    if (itemKey == none || !itemKey.IsValidName()) {
        _.logger.Auto(errItemInvalidName).ArgClass(itemClass).Arg(itemKey);
        return false;
    }
    // Guaranteed to only store cards
    existingCard = ItemCard(registeredCards.GetItem(itemName));
    if (existingCard != none) {
        _.logger.Auto(errItemDuplicate)
            .ArgClass(existingCard.GetItemClass())
            .Arg(itemKey)
            .ArgClass(itemClass);
        _.memory.Free(existingCard);
        return false;
    }
    newCard = MakeCard(itemClass, itemName);
    registeredCards.SetItem(itemKey, newCard);
    _.memory.Free2(itemKey, newCard);
    return true;
}

/// Removes item of given class from the list of registered items.
///
/// Removing once registered item is not an action that is expected to
/// be performed under normal circumstances and does not have an efficient
/// implementation (it is linear on the current amount of items).
///
/// Returns `true` if successfully removed registered item class and
/// `false` otherwise (either item wasn't registered or caller tool
/// initialized).
public function bool RemoveItemClass(class<AcediaObject> itemClass) {
    local int i;
    local CollectionIterator iter;
    local ItemCard nextCard;
    local array<Text> keysToRemove;

    if (itemClass == none)          return false;
    if (registeredCards == none)    return false;

    // Removing items during iterator breaks an iterator, so first we find
    // all the keys to remove
    iter = registeredCards.Iterate();
    iter.LeaveOnlyNotNone();
    while (!iter.HasFinished()) {
        // Guaranteed to only be `ItemCard`
        nextCard = ItemCard(iter.Get());
        if (nextCard.GetItemClass() == itemClass) {
            keysToRemove[keysToRemove.length] = Text(iter.GetKey());
            DiscardCard(nextCard);
        }
        _.memory.Free(nextCard);
        iter.Next();
    }
    iter.FreeSelf();
    // Actual clean up everything in `keysToRemove`
    for (i = 0; i < keysToRemove.length; i += 1) {
        registeredCards.RemoveItem(keysToRemove[i]);
    }
    _.memory.FreeMany(keysToRemove);
    return (keysToRemove.length > 0);
}

/// Allows to specify the order of the user group in terms of privilege for
/// accessing stored items. Only specified groups will be used when resolving
/// appropriate permissions config name for a user.
public final function SetPermissionGroupOrder(array<Text> groupOrder) {
    local int i;

    _.memory.FreeMany(permissionGroupOrder);
    permissionGroupOrder.length = 0;
    for (i = 0; i < groupOrder.length; i += 1) {
        if (groupOrder[i] != none) {
            permissionGroupOrder[permissionGroupOrder.length] = groupOrder[i].Copy();
        }
    }
}

/// Specifies what permissions (given by the config name) given user group has
/// when using an item with a specified name.
///
/// Method must be called after item with a given name is added.
///
/// If this config name is specified as `none`, then "default" will be
/// used instead. For non-`none` values, only an invalid name (according to
/// [`BaseText::IsValidName()`] method) will prevent the group from being
/// registered.
///
/// Method will return `true` if group was successfully authorized and `false`
/// otherwise (either group already authorized or no item with specified name
/// was added in the caller tool so far).
///
/// # Errors
///
/// If specified group was already authorized to use card's item, then it
/// will log a warning message about it.
public function bool AuthorizeUsage(BaseText itemName, BaseText groupName, BaseText configName) {
    local bool result;
    local ItemCard relevantCard;

    if (configName != none && !configName.IsValidName()) {
        return false;
    }
    relevantCard = GetCard(itemName);
    if (relevantCard != none) {
        result = relevantCard.AuthorizeGroupWithConfig(groupName, configName);
        _.memory.Free(relevantCard);
    }
    return result;
}

/// Returns struct with item class (+ instance, if one was stored) for a given
/// case in-sensitive item name and name of the config with best permissions
/// available to the player with provided ID.
///
/// Function only returns `none` for item class if item with a given name
/// wasn't found.
/// Config name being `none` with non-`none` item class in the result means
/// that user with provided ID doesn't have permissions for using the item at
/// all.
public final function CommandAPI.ItemConfigInfo ResolveItem(BaseText itemName, BaseText textID) {
    local int i;
    local ItemCard relevantCard;
    local CommandAPI.ItemConfigInfo result;

    relevantCard = GetCard(itemName);
    if (relevantCard == none) {
        // At this point contains `none` for all values -> indicates a failure
        // to find item in storage
        return result;
    }
    result.instance = relevantCard.GetItem();
    result.class = relevantCard.GetItemClass();
    if (textID == none) {
        return result;
    }
    // Look through all `permissionGroupOrder` in order to find most priviledged
    // group that user with `textID` belongs to
    for (i = 0; i < permissionGroupOrder.length && result.configName == none; i += 1) {
        if (_.users.IsSteamIDInGroup(textID, permissionGroupOrder[i])) {
            result.configName = relevantCard.GetConfigNameForGroup(permissionGroupOrder[i]);
        }
    }
    _.memory.Free(relevantCard);
    return result;
}

/// Returns all item classes that are stored inside caller tool.
///
/// Doesn't check for duplicates (although with a normal usage, there shouldn't
/// be any).
public final function array< class<AcediaObject> > GetAllItemClasses() {
    local array< class<AcediaObject> > result;
    local ItemCard value;
    local CollectionIterator iter;

    for (iter = registeredCards.Iterate(); !iter.HasFinished(); iter.Next()) {
        value = ItemCard(iter.Get());
        if (value != none) {
            result[result.length] = value.GetItemClass();
        }
        _.memory.Free(value);
    }
    iter.FreeSelf();
    return result;
}

/// Returns array of names of all available items.
public final function array<Text> GetItemsNames() {
    local array<Text> emptyResult;

    if (registeredCards != none) {
        return registeredCards.GetTextKeys();
    }
    return emptyResult;
}

/// Called each time a new card is to be created and stored.
///
/// Must be reimplemented by child classes.
protected function ItemCard MakeCard(class<AcediaObject> itemClass, BaseText itemName) {
    return none;
}

/// Called each time a certain card is to be removed from storage.
///
/// Must be reimplemented by child classes
/// (reimplementations SHOULD NOT DEALLOCATE `toDiscard`).
protected function DiscardCard(ItemCard toDiscard) {
}

/// Find item card for the item that was stored with a specified
/// case-insensitive name
///
/// Function only returns `none` if item with a given name wasn't found
/// (or `none` was provided as an argument).
protected final function ItemCard GetCard(BaseText itemName) {
    local Text itemKey;
    local ItemCard relevantCard;

    if (itemName == none)           return none;
    if (registeredCards == none)    return none;

    /// The item name is transformed into lowercase, immutable value.
    /// This facilitates the use of item names as keys in a [`HashTable`],
    /// enabling case-insensitive matching.
    itemKey = itemName.LowerCopy();
    relevantCard = ItemCard(registeredCards.GetItem(itemKey));
    _.memory.Free(itemKey);
    return relevantCard;
}

defaultproperties {
    errItemInvalidName = (l=LOG_Error,m="Attempt at registering item with class `%1` under an invalid name \"%2\" will be ignored.")
    errItemDuplicate = (l=LOG_Error,m="Command `%1` is already registered with name '%2'. Attempt at registering command `%3` with the same name will be ignored.")
}