/**
 *  Config class for storing map lists.
 *      Copyright   2020 bibibi
 *                  2020-2023 Shtoyan
 *                  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 UnflectApi extends AcediaObject;

//! This API offers advanced reflection capabilities for Unreal Script.
//!
//! Currently, the API supports the ability to replace the code of existing functions with
//! custom code.
//! This can greatly simplify the process of bug fixing and hooking into game events.

/// A variable responsible for replacing function code in real-time.
/// This variable is used to support dynamic function replacement/patching and event interception
/// at runtime.
var private UFunctionCast functionCaster;

/// Maps lower case function name (specifies by the full path "package.class.functionName")
/// to a `FunctionRule` that completely describes how it was replaced
var private HashTable completedReplacements;
/// Maps lower case function name (specifies by the full path "package.class.functionName")
/// to the `ByteArrayBox` with that function's original code.
var private HashTable originalScriptCodes;

var private LoggerAPI.Definition warnSameFunction;
var private LoggerAPI.Definition warnOverridingReplacement, errFailedToFindFunction;
var private LoggerAPI.Definition errReplacementWithoutSources, errCannotCreateReplacementRule;

protected function Constructor() {
    functionCaster = new class'UFunctionCast';
    completedReplacements = _.collections.EmptyHashTable();
    originalScriptCodes = _.collections.EmptyHashTable();
}

protected function Finalizer() {
    _drop();
    _.memory.Free(completedReplacements);
    _.memory.Free(originalScriptCodes);
    completedReplacements = none;
    originalScriptCodes = none;
    functionCaster = none;
}

public final function _drop() {
    local UFunction nextFunctionInstance;
    local Text nextFunctionName;
    local HashTableIterator iter;
    local ByteArrayBox nextSources;

    // Drop is called when Acedia is shutting down, so releasing references isn't necessary
    iter = HashTableIterator(completedReplacements.Iterate());
    while (!iter.HasFinished()) {
        nextFunctionInstance = none;
        nextFunctionName = Text(iter.GetKey());
        nextSources = ByteArrayBox(originalScriptCodes.GetItem(nextFunctionName));
        if (nextSources != none ) {
            nextFunctionInstance = FindFunction(nextFunctionName);
        }
        if (nextFunctionInstance != none) {
            nextFunctionInstance.script = nextSources.Get();
        }
        iter.Next();
    }
}

/// Reverts the replacement of the function's code, restoring its original behavior.
///
/// The function to be reverted should be specified using its full path, in the format
/// "package.class.functionName" (e.g. "KFMod.KFPawn.TossCash").
///
/// It's worth noting that several function replacements do not stack.
/// Even if [`ReplaceFunction()`] was called multiple times in a row to replace the same function,
/// this method will cancel all the changes at once.
///
/// This method returns true if the specified function was previously replaced and has now been
/// successfully reverted.
///
/// # Errors
///
/// If the specified function cannot be found (but [`functionName`] isn't `none`), or if
/// UnflectApi has not yet replaced it with any other function, this method will log an error.
public final function bool RevertFunction(BaseText functionName) {
    local bool result;
    local FunctionReplacement storedReplacement;
    local ByteArrayBox storedSources;
    local Text functionNameLowerCase;
    local UFunction functionInstance;
    local SideEffect sideEffect;

    if (functionName == none) {
        return false;
    }
    functionNameLowerCase = functionName.LowerCopy();
    storedReplacement = FunctionReplacement(completedReplacements.GetItem(functionNameLowerCase));
    if (storedReplacement != none) {
        storedSources = ByteArrayBox(originalScriptCodes.GetItem(functionNameLowerCase));
        if (storedSources == none) {
            _.logger.Auto(errReplacementWithoutSources).Arg(functionNameLowerCase.Copy());
        } else {
            functionInstance = FindFunction(functionNameLowerCase);
            if (functionInstance != none) {
                functionInstance.script = storedSources.Get();
                completedReplacements.RemoveItem(functionNameLowerCase);
                sideEffect = storedReplacement.GetSideEffect();
                _.sideEffects.RemoveInstance(sideEffect);
                result = true;
            } else {
                _.logger.Auto(errFailedToFindFunction).Arg(functionNameLowerCase.Copy());
            }
        }
    }
    _.memory.Free4(storedReplacement, functionNameLowerCase, storedSources, sideEffect);
    return result;
}

/// Reverts the replacement of the function's code, restoring its original behavior.
///
/// The function to be reverted should be specified using its full path, in the format
/// "package.class.functionName" (e.g. "KFMod.KFPawn.TossCash").
///
/// It's worth noting that several function replacements do not stack.
/// Even if [`ReplaceFunction()`] was called multiple times in a row to replace the same function,
/// this method will cancel all the changes at once.
///
/// This method returns true if the specified function was previously replaced and has now been
/// successfully reverted.
///
/// # Errors
///
/// If the specified function cannot be found (but [`functionName`] isn't `none`), or if
/// UnflectApi has not yet replaced it with any other function, this method will log an error.
public final function bool RevertFunction_S(string functionName) {
    local bool result;
    local MutableText wrapper;

    wrapper = _.text.FromStringM(functionName);
    result = RevertFunction(wrapper);
    _.memory.Free(wrapper);
    return result;
}

/// Determines whether the specified function has been replaced by UnflectApi.
///
/// The function to be checked should be specified using its full path, in the format
/// "package.class.functionName" (e.g. "KFMod.KFPawn.TossCash").
///
/// If the function has been replaced, this method will return `true`;
/// otherwise, it will return `false`.
public final function bool IsFunctionReplaced(BaseText functionName) {
    local bool result;
    local Text functionNameLowerCase;

    if (functionName == none) {
        return false;
    }
    functionNameLowerCase = functionName.LowerCopy();
    result = completedReplacements.HasKey(functionNameLowerCase);
    _.memory.Free(functionNameLowerCase);
    return result;
}

/// Determines whether the specified function has been replaced by UnflectApi.
///
/// The function to be checked should be specified using its full path, in the format
/// "package.class.functionName" (e.g. "KFMod.KFPawn.TossCash").
///
/// If the function has been replaced, this method will return `true`;
/// otherwise, it will return `false`.
public final function bool IsFunctionReplaced_S(string functionName) {
    local bool result;
    local MutableText wrapper;

    wrapper = _.text.FromStringM(functionName);
    result = IsFunctionReplaced(wrapper);
    _.memory.Free(wrapper);
    return result;
}

/// Replaces one function with another by modifying its script code in real-time.
///
/// The reason for replacement must be specified and will serve as a human-readable explanation
/// for a [SideEffect] associated with the replacement.
///
/// If you need to replace a function in a class, follow these steps:
///
/// 1. Create a new class that extends the class in which the function you want to replace is
///     located.
/// 2. Declare that function in the created class.
/// 3. **DO NOT** change the function declaration and argument types/amount.
/// 4. **DO NOT** create new local variables, as this can cause random crashes.
///     If you need additional variables, make them global and access them using the
///     `class'myNewClass'.default.myNewVariable` syntax.
/// 5. If you want to call or override parent code, make sure to always specify the desired parent
///     class name.
///     For example, use `super(TargetClass).PostBeginPlay()` instead of `super.PostBeginPlay()`.
///     This will prevent runaway loop crashes.
/// 6. Make your edits to the function's code, and then call the replacement function:
///     ```unrealscript
///     _.unflect.ReplaceFunction(
///         "package.class.targetFunction",
///         "myNewPackage.myNewClass.newFunction");
///     ```
///
/// Following these steps will help ensure that your code changes are compatible with the rest of
/// the codebase and do not cause unexpected crashes.
///
/// # Errors
///
/// This method can log error messages in cases where:
///
/// * The specified function(s) cannot be found.
/// * An attempt is made to replace a function with itself.
/// * An attempt is made to replace a function that has already been replaced.
public final function bool ReplaceFunction(
    BaseText oldFunction,
    BaseText newFunction,
    BaseText replacementReason
) {
    local bool result;
    local Text oldFunctionLowerCase, newFunctionLowerCase;

    if (oldFunction == none)    return false;
    if (newFunction == none)    return false;

    oldFunctionLowerCase = oldFunction.LowerCopy();
    newFunctionLowerCase = newFunction.LowerCopy();
    result = _replaceFunction(oldFunctionLowerCase, newFunctionLowerCase);
    if (result) {
        RecordNewReplacement(oldFunctionLowerCase, newFunctionLowerCase, replacementReason);
    }
    _.memory.Free2(oldFunctionLowerCase, newFunctionLowerCase);
    return result;
}

/// Replaces one function with another by modifying its script code in real-time.
///
/// The reason for replacement must be specified and will serve as a human-readable explanation
/// for a [SideEffect] associated with the replacement.
///
/// If you need to replace a function in a class, follow these steps:
///
/// 1. Create a new class that extends the class in which the function you want to replace is
///     located.
/// 2. Declare that function in the created class.
/// 3. **DO NOT** change the function declaration and argument types/amount.
/// 4. **DO NOT** create new local variables, as this can cause random crashes.
///     If you need additional variables, make them global and access them using the
///     `class'myNewClass'.default.myNewVariable` syntax.
/// 5. If you want to call or override parent code, make sure to always specify the desired parent
///     class name.
///     For example, use `super(TargetClass).PostBeginPlay()` instead of `super.PostBeginPlay()`.
///     This will prevent runaway loop crashes.
/// 6. Make your edits to the function's code, and then call the replacement function:
///     ```unrealscript
///     _.unflect.ReplaceFunction(
///         "package.class.targetFunction",
///         "myNewPackage.myNewClass.newFunction");
///     ```
///
/// Following these steps will help ensure that your code changes are compatible with the rest of
/// the codebase and do not cause unexpected crashes.
///
/// # Errors
///
/// This method can log error messages in cases where:
///
/// * The specified function(s) cannot be found.
/// * An attempt is made to replace a function with itself.
/// * An attempt is made to replace a function that has already been replaced.
public final function bool ReplaceFunction_S(
    string oldFunction,
    string newFunction,
    string replacementReason
) {
    local Text oldWrapper, newWrapper, reasonWrapper;
    local bool result;

    oldWrapper = _.text.FromString(oldFunction);
    newWrapper = _.text.FromString(newFunction);
    reasonWrapper = _.text.FromString(replacementReason);
    result = ReplaceFunction(oldWrapper, newWrapper, reasonWrapper);
    _.memory.Free3(oldWrapper, newWrapper, reasonWrapper);
    return result;
}

// Does actual work for function replacement.
// Arguments are assumed to be not `none` and in lower case.
private final function bool _replaceFunction(Text oldFunctionLowerCase, Text newFunctionLowerCase) {
    local ByteArrayBox initialCode;
    local UFunction replace, with;

    replace = FindFunction(oldFunctionLowerCase);
    if (replace == none) {
        _.logger.Auto(errFailedToFindFunction).Arg(oldFunctionLowerCase.Copy());
        return false;
    }
    with = FindFunction(newFunctionLowerCase);
    if (with == none) {
        _.logger.Auto(errFailedToFindFunction).Arg(newFunctionLowerCase.Copy());
        return false;
    }
    if (replace == with) {
        _.logger.Auto(warnSameFunction).Arg(oldFunctionLowerCase.Copy());
        return false;
    }
    // Remember old code, if haven't done so yet.
    // Since we attempt it on each replacement, the first recorded `script` value will be
    // the initial code.
    if (!originalScriptCodes.HasKey(oldFunctionLowerCase)) {
        initialCode = _.box.ByteArray(replace.script);
        originalScriptCodes.SetItem(oldFunctionLowerCase, initialCode);
        _.memory.Free(initialCode);
    }
    replace.script = with.script;
    return true;
}

// Arguments assumed to be not `none` and in lower case.
private final function UFunction FindFunction(Text functionNameLowerCase) {
    local string stringName;

    stringName = functionNameLowerCase.ToString();
    // We need to make sure functions are loaded before performing the replacement.
    DynamicLoadObject(GetClassName(stringName), class'class', true);
    return functionCaster.Cast(function(FindObject(stringName, class'Function')));
}

// Arguments are assumed to be not `none`.
// `oldFunctionLowerCase` and `newFunctionLowerCase` are assumed to be in lower case.
private final function RecordNewReplacement(
    Text oldFunctionLowerCase,
    Text newFunctionLowerCase,
    BaseText replacementReason
) {
    local SideEffect oldSideEffect, newSideEffect;
    local FunctionReplacement oldRule, newRule;

    // Remove old `FunctionReplacement`, if there is any
    oldRule = FunctionReplacement(completedReplacements.GetItem(oldFunctionLowerCase));
    if (oldRule != none) {
        _.logger
            .Auto(warnOverridingReplacement)
            .Arg(oldFunctionLowerCase.Copy())
            .Arg(oldRule.GetReplacerFunctionName())
            .Arg(newFunctionLowerCase.Copy());
        oldSideEffect = oldRule.GetSideEffect();
        _.sideEffects.RemoveInstance(oldSideEffect);
        _.memory.Free2(oldRule, oldSideEffect);
    }
    // Create replacement instance
    newSideEffect = MakeSideEffect(oldFunctionLowerCase, newFunctionLowerCase, replacementReason);
    newRule = class'FunctionReplacement'.static
        .Make(oldFunctionLowerCase, newFunctionLowerCase, newSideEffect);
    completedReplacements.SetItem(oldFunctionLowerCase, newRule);
    if (newRule == none) {
        _.logger
            .Auto(errCannotCreateReplacementRule)
            .Arg(oldFunctionLowerCase.Copy())
            .Arg(newFunctionLowerCase.Copy());
    }
}

// Arguments are assumed to be not `none`.
// `oldFunctionLowerCase` and `newFunctionLowerCase` are assumed to be in lower case.
private final function SideEffect MakeSideEffect(
    Text oldFunctionLowerCase,
    Text newFunctionLowerCase,
    BaseText replacementReason
) {
    local SideEffect sideEffect;
    local MutableText status;

    // Add side effect
    status = oldFunctionLowerCase.MutableCopy();
    status.Append(P(" -> "));
    status.Append(newFunctionLowerCase);
    sideEffect = _.sideEffects.Add(
        P("Changed function's code"),
        replacementReason,
        P("AcediaCore"),
        P("UnflectAPI"),
        status
    );
    _.memory.Free(status);
    return sideEffect;
}

// Get the "package + dot + class" string for DynamicLoadObject()
private final static function string GetClassName(string input) {
    local array<string> parts;

    // create an array
    Split(input, ".", parts);

    // state functions
    if (parts.length < 3) {
        return "";
    }
    if (parts.length == 4) {
        ReplaceText(input, "." $ parts[2], "");
        ReplaceText(input, "." $ parts[3], "");
    } else {
        ReplaceText(input, "." $ parts[2], "");
    }
    return input;
}


defaultproperties {
    warnOverridingReplacement = (l=LOG_Error,m="Attempt to replace a function `%1` with function `%3` after it has already been replaced with `%2`.")
    warnSameFunction = (l=LOG_Error,m="Attempt to replace a function `%1` with itself.")
    errFailedToFindFunction = (l=LOG_Error,m="`UnflectApi` has failed to find function `%1`.")
    errReplacementWithoutSources = (l=LOG_Error,m="Cannot restore function `%1` - its initial source code wasn't preserved. This most likely means that it wasn't yet replaced.")
    errCannotCreateReplacementRule = (l=LOG_Error,m="`Cannot create new rule for replacing function `%1` with `%2` even though code was successfully replaces. This should happen, please report this.")
}