/// ----------------------------------------------------------------------------
/// @file   util_i_libraries.nss
/// @author Michael A. Sinclair (Squatting Monk) <squattingmonk@gmail.com>
/// @author Ed Burke (tinygiant98) <af.hog.pilot@gmail.com>
/// @brief  This file holds functions for packing scripts into libraries. This
///     allows the builder to dramatically reduce the module script count by
///     keeping related scripts in the same file.
/// @details
/// Libraries allow the builder to encapsulate many scripts into one,
/// dramatically reducing the script count in the module. In a library, each
/// script is a function bound to a unique name and/or number. When the library
/// is called, the name is routed to the proper function.
///
/// Since each script defined by a library has a unique name to identify it, the
/// builder can execute a library script without having to know the file it is
/// located in. This makes it easy to create script systems to override behavior
/// of another system; you don't have to edit the other system's code, you just
/// implement your own function to override it.
///
/// ## Anatomy of a Library
/// This is an example of a simple library:
///
/// ``` nwscript
/// #include "util_i_libraries"
///
/// void MyFunction()
/// {
///     // ...
/// }
///
/// void MyOtherFunction()
/// {
///     // ...
/// }
///
/// void OnLibraryLoad()
/// {
///     RegisterLibraryScript("MyFunction");
///     RegisterLibraryScript("MyOtherFunction");
/// }
/// ```
///
/// This script contains custom functions (`MyFunction()` and `MyOtherFunction()`)
/// as well as an `OnLibraryLoad()` function. `OnLibraryLoad()` is executed
/// whenever the library is loaded by `LoadLibrary()`; it calls
/// `RegisterLibraryScript()` to expose the names of the custom functions as
/// library scripts. When a library script is called with `RunLibraryScript()`,
/// the custom functions are called.
///
/// If you want to do something more complicated that can't be handled by a
/// single function call, you can pass a unique number to
/// `RegisterLibraryScript()` as its second parameter, which will cause
/// `RunLibraryScript()` to call a special customizable dispatching function
/// called `OnLibraryScript()`. This function takes the name and number of the
/// desired function and executes the desired code. For example:
///
/// ``` nwscript
/// #include "util_i_libraries"
///
/// void OnLibraryLoad()
/// {
///     RegisterLibraryScript("Give50GP",  1);
///     RegisterLibraryScript("Give100GP", 2);
/// }
///
/// void OnLibraryScript(string sScript, int nEntry)
/// {
///     switch (nEntry)
///     {
///         case 1: GiveGoldToCreature(OBJECT_SELF, 50); break;
///         case 2: GiveGoldToCreature(OBJECT_SELF, 100); break;
///     }
/// }
/// ```
///
/// **Note:** A library does not need to have a `main()` function, because this
/// will be automatically generated by the `LoadLibrary()` and
/// `RunLibraryScript()` functions.
///
/// ## Using a Library
/// `util_i_libraries.nss` is needed to load or run library scripts.
///
/// To use a library, you must first load it. This will activate the library's
/// `OnLibraryLoad()` function and register each desired function.
///
/// ``` nwscript
/// // Loads a single library
/// LoadLibrary("my_l_library");
///
/// // Loads a CSV list of library scripts
/// LoadLibraries("pw_l_plugin, dlg_l_example, prr_l_main");
///
/// // Loads all libraries matching a glob pattern
/// LoadLibrariesByPattern("*_l_*");
///
/// // Loads all libraries matching a prefix
/// LoadLibrariesByPrefix("pw_l_");
/// ```
///
/// If a library implements a script that has already been implemented in
/// another library, a warning will be issued and the newer script will take
/// precedence.
///
/// Calling a library script is done using `RunLibraryScript()`. The name
/// supplied should be the name bound to the function in the library's
/// `OnLibraryLoad()`. If the name supplied is implemented by a library, the
/// library will be JIT compiled and the desired function will be called with
/// `ExecuteScriptChunk()`. Otherwise, the name will be assumed to match a
/// normal script, which will be executed with `ExecuteScript()`.
///
/// ``` nwscript
/// // Executes a single library script on OBJECT_SELF
/// RunLibraryScript("MyFunction");
///
/// // Executes a CSV list of library scripts, for which oPC will be OBJECT_SELF
/// object oPC = GetFirstPC();
/// RunLibraryScripts("MyFunction, MyOtherFunction", oPC);
/// ```
///
/// ## Pre-Compiled Libraries
/// By default, libraries are run using `ExecuteScriptChunk()`, which JIT
/// compiles the script and runs it each time the library script is called. If
/// you wish to have your script pre-compiled, you can include the script
/// `util_i_library.nss` in your file in place of `util_i_libraries.nss`. This
/// script contains a `main()` function that will call either your
/// `OnLibraryLoad()` or `OnLibraryScript()` function as appropriate; thus, if
/// you use this method, you *must* provide an `OnLibraryScript()` dispatch
/// function.
///
/// **Note**: `util_i_library.nss` uses the nwnsc `default_function` pragma to
/// prevent compilation errors and will not compile with the toolset compiler.
/// If this is not desired, you can either comment those lines out or implement
/// the `main()` function yourself.
/// ----------------------------------------------------------------------------

#include "util_i_debug"
#include "util_i_csvlists"
#include "util_i_sqlite"
#include "util_i_nss"
#include "util_i_matching"

// -----------------------------------------------------------------------------
//                                   Constants
// -----------------------------------------------------------------------------

const string LIB_RETURN  = "LIB_RETURN";  ///< The return value of the library
const string LIB_LIBRARY = "LIB_LIBRARY"; ///< The library being processed
const string LIB_SCRIPT  = "LIB_SCRIPT";  ///< The library script name
const string LIB_ENTRY   = "LIB_ENTRY";   ///< The library script entry number

// -----------------------------------------------------------------------------
//                              Function Prototypes
// -----------------------------------------------------------------------------

/// @brief Create a library table in the module's volatile sqlite database.
/// @param bReset if TRUE, the table will be dropped if already present.
/// @note This is called automatically by the library functions.
void CreateLibraryTable(int bReset = FALSE);

/// @brief Add a database record associating a script with a library.
/// @param sLibrary The script to source from.
/// @param sScript The name to associate with the library script.
/// @param nEntry A number unique to sLibrary to identify this script. If this
///     is 0 and the library has not been pre-compiled, RunLibraryScript() will
///     call sScript directly. Otherwise, RunLibraryScript() will run a dispatch
///     function that can use this number to execute the correct code. Thus,
///     nEntry must be set if sScript does not exactly match the desired
///     function name or the function requires parameters.
void AddLibraryScript(string sLibrary, string sScript, int nEntry = 0);

/// @brief Return the name of the library containing a script from the database.
/// @param sScript The name of the library script.
string GetScriptLibrary(string sScript);

/// @brief Return the entry number associated with a library script.
/// @param sScript The name of the library script.
int GetScriptEntry(string sScript);

/// @brief Return a prepared query with the with the library and entry data
///     associated with a library script.
/// @param sScript The name of the library script.
/// @note This allows users to retrive the same data returned by
///     GetScriptLibrary() and GetScriptEntry() with one function.
sqlquery GetScriptData(string sScript);

/// @brief Return whether a script library has been loaded.
/// @param sLibrary The name of the script library file.
int GetIsLibraryLoaded(string sLibrary);

/// @brief Load a script library by executing its OnLibraryLoad() function.
/// @param sLibrary The name of the script library file.
/// @param bForce If TRUE, will re-load the library if it was already loaded.
void LoadLibrary(string sLibrary, int bForce = FALSE);

/// @brief Load a list of script libraries in sequence.
/// @param sLibraries A CSV list of libraries to load.
/// @param bForce If TRUE, will re-load the library if it was already loaded.
void LoadLibraries(string sLibraries, int bForce = FALSE);

/// @brief Return a json array of script names with a prefix.
/// @param sPrefix The prefix matching the scripts to find.
/// @returns A sorted json array of script names, minus the extensions.
/// @note The search includes both nss and ncs files, with duplicates removed.
json GetScriptsByPrefix(string sPrefix);

/// @brief Load all scripts matching the given glob pattern(s).
/// @param sPattern A CSV list of glob patterns to match with. Supported syntax:
///     - `*`: match zero or more characters
///     - `?`: match a single character
///     - `[abc]`: match any of a, b, or c
///     - `[a-z]`: match any character from a-z
///     - other text is matched literally
/// @param bForce If TRUE, will-reload the library if it was already loaded.
void LoadLibrariesByPattern(string sPattern, int bForce = FALSE);

/// @brief Load all scripts with a given prefix as script libraries.
/// @param sPrefix A prefix for the desired script libraries.
/// @param bForce If TRUE, will re-load the library if it was already loaded.
/// @see GetMatchesPattern() for the rules on glob syntax.
void LoadLibrariesByPrefix(string sPrefix, int bForce = FALSE);

/// @brief Execute a registered library script.
/// @param sScript The unique name of the library script.
/// @param oSelf The object that should execute the script as OBJECT_SELF.
/// @returns The integer value set with LibraryReturn() by sScript.
/// @note If sScript is not registered as a library script, it will be executed
///     as a regular script instead.
int RunLibraryScript(string sScript, object oSelf = OBJECT_SELF);

/// @brief Execute a list of registered library scripts in sequence.
/// @param sScripts A CSV list of library script names.
/// @param oSelf The object that should execute the scripts as OBJECT_SELF.
/// @note If any script in sScripts is not registered as a library script, it
///     will be executed as a regular script instead.
void RunLibraryScripts(string sScripts, object oSelf = OBJECT_SELF);

/// @brief Register a script to a library. The script can later be called using
///     RunLibraryScript().
/// @param sScript A name for the script. Must be unique in the module. If a
///     second script with the same name is registered, it will overwrite the
///     first one.  This value does not have to match the function or script name.
/// @param nEntry A number unique to this library to identify this script. If
///     this is 0 and the library has not been pre-compiled, RunLibraryScript()
///     will call sScript directly. Otherwise, RunLibraryScript() will run a
///     dispatch function that can use this number to execute the correct code.
///     Thus, nEntry must be set if sScript does not exactly match the desired
///     function name or the function requires parameters.
/// @note Must be called within a script library's OnLibraryLoad() function. For
///     uses in other places, use AddLibraryScript().
void RegisterLibraryScript(string sScript, int nEntry = 0);

/// @brief Set the return value of the currently executing library script.
/// @param nValue The value to return to the calling script.
void LibraryReturn(int nValue);

// -----------------------------------------------------------------------------
//                             Function Definitions
// -----------------------------------------------------------------------------

void CreateLibraryTable(int bReset = FALSE)
{
    SqlCreateTableModule("library_scripts",
        "id INTEGER PRIMARY KEY AUTOINCREMENT, " +
        "sLibrary TEXT NOT NULL, " +
        "sScript TEXT NOT NULL UNIQUE ON CONFLICT REPLACE, " +
        "nEntry INTEGER NOT NULL);",
        bReset);
}

void AddLibraryScript(string sLibrary, string sScript, int nEntry = 0)
{
    CreateLibraryTable();

    string sQuery = "INSERT INTO library_scripts (sLibrary, sScript, nEntry) " +
                    "VALUES (@sLibrary, @sScript, @nEntry);";
    sqlquery sql = SqlPrepareQueryModule(sQuery);
    SqlBindString(sql, "@sLibrary", sLibrary);
    SqlBindString(sql, "@sScript", sScript);
    SqlBindInt(sql, "@nEntry", nEntry);

    SqlStep(sql);
}

string GetScriptFieldData(string sField, string sScript)
{
    CreateLibraryTable();

    string sQuery = "SELECT " + sField + " FROM library_scripts " +
                    "WHERE sScript = @sScript;";
    sqlquery sql = SqlPrepareQueryModule(sQuery);
    SqlBindString(sql, "@sScript", sScript);

    return SqlStep(sql) ? SqlGetString(sql, 0) : "";
}

string GetScriptLibrary(string sScript)
{
    return GetScriptFieldData("sLibrary", sScript);
}

int GetScriptEntry(string sScript)
{
    return StringToInt(GetScriptFieldData("nEntry", sScript));
}

sqlquery GetScriptData(string sScript)
{
    CreateLibraryTable();

    string sQuery = "SELECT sLibrary, nEntry FROM library_scripts " +
                    "WHERE sScript = @sScript;";
    sqlquery sql = SqlPrepareQueryModule(sQuery);
    SqlBindString(sql, "@sScript", sScript);

    return sql;
}

int GetIsLibraryLoaded(string sLibrary)
{
    CreateLibraryTable();

    string sQuery = "SELECT COUNT(sLibrary) FROM library_scripts " +
                    "WHERE sLibrary = @sLibrary LIMIT 1;";
    sqlquery sql = SqlPrepareQueryModule(sQuery);
    SqlBindString(sql, "@sLibrary", sLibrary);

    return SqlStep(sql) ? SqlGetInt(sql, 0) : FALSE;
}

void LoadLibrary(string sLibrary, int bForce = FALSE)
{
    Debug("Attempting to " + (bForce ? "force " : "") + "load library " + sLibrary);

    if (bForce || !GetIsLibraryLoaded(sLibrary))
    {
        SetScriptParam(LIB_LIBRARY, sLibrary);
        if (ResManGetAliasFor(sLibrary, RESTYPE_NCS) == "")
        {
            Debug(sLibrary + ".ncs not present; loading library as chunk");
            string sChunk = NssInclude(sLibrary) + NssVoidMain(NssFunction("OnLibraryLoad"));
            string sError = ExecuteScriptChunk(sChunk, GetModule(), FALSE);
            if (sError != "")
                CriticalError("Could not load " + sLibrary + ": " + sError);
        }
        else
            ExecuteScript(sLibrary, GetModule());

    }
    else
        Error("Library " + sLibrary + " already loaded!");
}

void LoadLibraries(string sLibraries, int bForce = FALSE)
{
    Debug("Attempting to " + (bForce ? "force " : "") + "load libraries " + sLibraries);

    int i, nCount = CountList(sLibraries);
    for (i = 0; i < nCount; i++)
        LoadLibrary(GetListItem(sLibraries, i), bForce);
}

// Private function for GetScriptsByPrefix*(). Adds all scripts of nResType
// matching a prefix to a json array and returns it.
json _GetScriptsByPrefix(json jArray, string sPrefix, int nResType)
{
    int i;
    string sScript;
    while ((sScript = ResManFindPrefix(sPrefix, nResType, ++i)) != "")
        jArray = JsonArrayInsert(jArray, JsonString(sScript));

    return jArray;
}

json GetScriptsByPrefix(string sPrefix)
{
    json jScripts = _GetScriptsByPrefix(JsonArray(), sPrefix, RESTYPE_NCS);
         jScripts = _GetScriptsByPrefix(jScripts,    sPrefix, RESTYPE_NSS);
         jScripts = JsonArrayTransform(jScripts, JSON_ARRAY_UNIQUE);
         jScripts = JsonArrayTransform(jScripts, JSON_ARRAY_SORT_ASCENDING);
    return jScripts;
}

void LoadLibrariesByPattern(string sPatterns, int bForce = FALSE)
{
    if (sPatterns == "")
        return;

    Debug("Finding libraries matching \"" + sPatterns + "\"");
    json jPatterns  = ListToJson(sPatterns);
    json jLibraries = FilterByPatterns(GetScriptsByPrefix(""), jPatterns, TRUE);
    LoadLibraries(JsonToList(jLibraries), bForce);
}

void LoadLibrariesByPrefix(string sPrefix, int bForce = FALSE)
{
    Debug("Finding libraries with prefix \"" + sPrefix + "\"");
    json jLibraries = GetScriptsByPrefix(sPrefix);
    LoadLibraries(JsonToList(jLibraries), bForce);
}

void LoadPrefixLibraries(string sPrefix, int bForce = FALSE)
{
    Debug("LoadPrefixLibraries() is deprecated; use LoadLibrariesByPrefix()");
    LoadLibrariesByPrefix(sPrefix, bForce);
}

int RunLibraryScript(string sScript, object oSelf = OBJECT_SELF)
{
    if (sScript == "") return -1;

    string sLibrary;
    int nEntry;

    sqlquery sqlScriptData = GetScriptData(sScript);
    if (SqlStep(sqlScriptData))
    {
        sLibrary = SqlGetString(sqlScriptData, 0);
        nEntry = SqlGetInt(sqlScriptData, 1);
    }

    DeleteLocalInt(oSelf, LIB_RETURN);

    if (sLibrary != "")
    {
        Debug("Library script " + sScript + " found in " + sLibrary +
            (nEntry != 0 ? " at entry " + IntToString(nEntry) : ""));

        SetScriptParam(LIB_LIBRARY, sLibrary);
        SetScriptParam(LIB_SCRIPT, sScript);
        SetScriptParam(LIB_ENTRY, IntToString(nEntry));

        if (ResManGetAliasFor(sLibrary, RESTYPE_NCS) == "")
        {
            Debug(sLibrary + ".ncs not present; running library script as chunk");
            string sChunk = NssInclude(sLibrary) + NssVoidMain(nEntry ?
                NssFunction("OnLibraryScript", NssQuote(sScript) + ", " + IntToString(nEntry)) :
                NssFunction(sScript));
            string sError = ExecuteScriptChunk(sChunk, oSelf, FALSE);
            if (sError != "")
                CriticalError("RunLibraryScript(" + sScript +") failed: " + sError);
        }
        else
            ExecuteScript(sLibrary, oSelf);
    }
    else
    {
        Debug(sScript + " is not a library script; executing directly");
        ExecuteScript(sScript, oSelf);
    }

    return GetLocalInt(oSelf, LIB_RETURN);
}

void RunLibraryScripts(string sScripts, object oSelf = OBJECT_SELF)
{
    int i, nCount = CountList(sScripts);
    for (i = 0; i < nCount; i++)
        RunLibraryScript(GetListItem(sScripts, i), oSelf);
}

void RegisterLibraryScript(string sScript, int nEntry = 0)
{
    string sLibrary = GetScriptParam(LIB_LIBRARY);
    string sExist = GetScriptLibrary(sScript);

    if (sLibrary != sExist && sExist != "")
        Warning(sLibrary + " is overriding " + sExist + "'s implementation of " + sScript);

    int nOldEntry = GetScriptEntry(sScript);
    if (nOldEntry)
        Warning(sLibrary + " already declared " + sScript +
            " Old Entry: " + IntToString(nOldEntry) +
            " New Entry: " + IntToString(nEntry));

    AddLibraryScript(sLibrary, sScript, nEntry);
}

void LibraryReturn(int nValue)
{
    SetLocalInt(OBJECT_SELF, LIB_RETURN, nValue);
}