//:://////////////////////////////////////////////
//:: Thread include
//:: inc_threads
//:://////////////////////////////////////////////
/** @file
    A simple set of functions for creating,
    controlling and destroying threads that
    repeatedly run a given script.
    A thread is implemented as a pseudo-hb that
    executes a given script on each of it's
    beats.

    Threads may be in one of 3 states:
     THREAD_STATE_DEAD:
      Equivalent to the thread not existing at
      all.

     THREAD_STATE_RUNNING:
      The thread is alive, and will execute it's
      script on each of the underlying pseudo-hb's
      beats.

     THREAD_STATE_SLEEPING:
      The thread is alive, but will not execute
      it's script on the pseudo-hb's beats.


    The thread's script will be ExecuteScripted on
    the object that the thread is running on. This
    is the same object that also stores the
    thread's state (all of 3 local variables).


    @author Ornedan
    @date   Created - 14.03.2005
*/
//:://////////////////////////////////////////////
//:://////////////////////////////////////////////


//////////////////////////////////////////////////
/* Constant declarations                        */
//////////////////////////////////////////////////

// Thread state constants

/// Thread state - Dead or non-existent
const int THREAD_STATE_DEAD     = 0;
/// Thread state - Running at the moment
const int THREAD_STATE_RUNNING  = 1;
/// Thread state - Sleeping
const int THREAD_STATE_SLEEPING = 2;


// Internal constants. Nothing to see here. <.<  >.>

const string PREFIX           = "prc_thread_";
const string SUFFIX_SCRIPT    = "_script";
const string SUFFIX_INTERVAL  = "_interval";
const string SUFFIX_ITERATION = "_iteration";
const string CUR_THREAD       = "current_thread";


//////////////////////////////////////////////////
/* Function prototypes                          */
//////////////////////////////////////////////////

/**
 * Creates a new thread.
 *
 * @param sName                Name of thread to create. Must be non-empty
 * @param sScript              Name of script to run. Must be non-empty
 * @param fExecutionInterval   Amount of time that passes between executions of sScript.
 *                             Only values > 0.0 allowed
 * @param oToRunOn             Object that stores the thread state values, and that
 *                             sScript will be ExecuteScripted on.
 *                             If this is OBJECT_INVALID, the module will be used to hold
 *                             the thread
 *
 * @return                     TRUE if the thread creation was successfull. Possible reasons of failure:
 *                              - One or more parameters were invalid
 *                              - A thread by the given name was already running on oToRunOn
 */
int SpawnNewThread(string sName, string sScript, float fExecutionInterval = 6.0f, object oToRunOn = OBJECT_INVALID);

/**
 * Inspects the state of the given thread.
 *
 * @param sName      Name of thread to inspect. Must be non-empty
 * @param oRunningOn Object that the thread is running on. If this
 *                   is OBJECT_INVALID, the module will be used.
 *
 * @return           One of the THREAD_STATE_* constants. Inspecting a non-
 *                   existent thread, or thread that was running on an object that
 *                   was destroyed will return THREAD_STATE_DEAD
 */
int GetThreadState(string sName, object oRunningOn = OBJECT_INVALID);

/**
 * Gets the name of the script the given thread is running.
 *
 * @param sName      Name of thread to inspect. Must be non-empty
 * @param oRunningOn Object that the thread is running on. If this
 *                   is OBJECT_INVALID, the module will be used.
 *
 * @return           The name of the the given thread executes on success, ""
 *                   on failure (querying with invalid parameters, or on a dead thread)
 */
string GetThreadScript(string sName, object oRunningOn = OBJECT_INVALID);

/** Gets the execution interval for the given thread
 *
 * @param sName      Name of thread to inspect. Must be non-empty
 * @param oRunningOn Object that the thread is running on. If this
 *                   is OBJECT_INVALID, the module will be used.
 *
 * @return           The time between the given thread executing it's script.
 *                   On failure, 0.0f is returned.
 */
float GetThreadExecutionInterval(string sName, object oRunningOn = OBJECT_INVALID);

/**
 * Gets the name of the thread whose script is currently being executed.
 *
 * @return The name of the thread being executed at the time of the call,
 *         or "" if no thread is being executed when this is called.
 */
string GetCurrentThread();

/**
 * Gets the object currently running thread is executing on
 *
 * @return The object the currently executing thread is being executed on
 *         or OBJECT_INVALID if no thread is being executed when this is called.
 */
object GetCurrentThreadObject();

/**
 * Stops further execution of the given thread and removes it's data
 * from the object it was running on.
 *
 * @param sName      Name of thread to terminate. Must be non-empty
 * @param oRunningOn Object that the thread is running on. If this
 *                   is OBJECT_INVALID, the module will be used.
 */
void TerminateThread(string sName, object oRunningOn = OBJECT_INVALID);

/**
 * Stops further execution of the thread currently being executed.
 * A convenience wrapper for TerminateThread to be called from a
 * threadscript.
 */
void TerminateCurrentThread();

/**
 * Sets the stae of the given thread to sleeping.
 *
 * @param sName      Name of thread to set sleeping. Must be non-empty
 * @param oRunningOn Object that the thread is running on. If this
 *                   is OBJECT_INVALID, the module will be used.
 *
 * @return           Whether the operation was successfull. Failure indicates
 *                   that the thread was dead.
 */
int SleepThread(string sName, object oRunningOn = OBJECT_INVALID);

/**
 * Awakens the given thread.
 *
 * @param sName      Name of thread to set back running. Must be non-empty
 * @param oRunningOn Object that the thread is running on. If this
 *                   is OBJECT_INVALID, the module will be used.
 *
 * @return           Whether the operation was successfull. Failure indicates
 *                   that the thread was dead.
 */
int AwakenThread(string sName, object oRunningOn = OBJECT_INVALID);

/**
 * Changes the execution interval of the given thread.
 *
 * @param sName        Name of thread to set back running. Must be non-empty
 * @param oRunningOn   Object that the thread is running on. If this
 *                     is OBJECT_INVALID, the module will be used.
 * @param fNewInterval The amount of time between executions of the
 *                     threadscript that will used from next execution
 *                     onwards.
 *
 * @return             Returns whether the operation was successfull. Failure indicates
 *                     that the thread was dead.
 */
int ChangeExecutionInterval(string sName, float fNewInterval, object oRunningOn = OBJECT_INVALID);

/*
 * Internal function. This is the pseudo-hb function that calls itself.
 *
 * @param sName        name of the thread to run. Used to build local variable names
 * @param oRunningOn   object that stores the variables, and the one that will
 *                     be passed to ExecuteScript
 */
void RunThread(string sName, object oRunningOn, int iIteration);


//////////////////////////////////////////////////
/* Function defintions                          */
//////////////////////////////////////////////////


int SpawnNewThread(string sName, string sScript, float fExecutionInterval = 6.0f, object oToRunOn = OBJECT_INVALID){
    if(oToRunOn == OBJECT_INVALID)
    	oToRunOn = GetModule();

    // Check paramaeters for correctness
    if(sName   == ""              ||
       sScript == ""              ||
       fExecutionInterval <= 0.0f ||
       !GetIsObjectValid(oToRunOn))
        return FALSE;

    // Make sure there is no thread by this name already running
    //    if(GetLocalInt(oToRunOn, PREFIX + sName))
    //    return FALSE;
    // use iterations in place of the above to make it more reliable in case of a PC thread timing out while not logged in
    int iIteration = GetLocalInt(oToRunOn, PREFIX + sName + SUFFIX_ITERATION);

    // Set the thread variables
    SetLocalInt(oToRunOn,    PREFIX + sName, THREAD_STATE_RUNNING);
    SetLocalString(oToRunOn, PREFIX + sName + SUFFIX_SCRIPT, sScript);
    SetLocalFloat(oToRunOn,  PREFIX + sName + SUFFIX_INTERVAL, fExecutionInterval);

    // Start thread execution
    DelayCommand(fExecutionInterval, RunThread(sName, oToRunOn, iIteration));

    // All done successfully
    return TRUE;
}


int GetThreadState(string sName, object oRunningOn = OBJECT_INVALID){
    if(oRunningOn == OBJECT_INVALID)
        oRunningOn = GetModule();

    // Check paramaeters for correctness
    if(sName == "" ||
       !GetIsObjectValid(oRunningOn))
        return FALSE;

    // Return the local determining if the thread exists
    return GetLocalInt(oRunningOn, PREFIX + sName);
}


string GetThreadScript(string sName, object oRunningOn = OBJECT_INVALID){
    if(oRunningOn == OBJECT_INVALID)
        oRunningOn = GetModule();

    // Check paramaeters for correctness
    if(sName == "" ||
       !GetIsObjectValid(oRunningOn))
        return "";

    return GetLocalString(oRunningOn, PREFIX + sName + SUFFIX_SCRIPT);
}


float GetThreadExecutionInterval(string sName, object oRunningOn = OBJECT_INVALID){
    if(oRunningOn == OBJECT_INVALID)
        oRunningOn = GetModule();

    // Check paramaeters for correctness
    if(sName == "" ||
       !GetIsObjectValid(oRunningOn))
        return 0.0f;

    return GetLocalFloat(oRunningOn, PREFIX + sName + SUFFIX_INTERVAL);
}


string GetCurrentThread(){
    return GetLocalString(GetModule(), PREFIX + CUR_THREAD);
}


object GetCurrentThreadObject(){
    return GetIsObjectValid(GetLocalObject(GetModule(), PREFIX + CUR_THREAD)) ?
            GetLocalObject(GetModule(), PREFIX + CUR_THREAD) :
            OBJECT_INVALID;
}


void TerminateThread(string sName, object oRunningOn = OBJECT_INVALID){
    if(oRunningOn == OBJECT_INVALID)
        oRunningOn = GetModule();

    // Check paramaeters for correctness. Just an optimization here, since
    // if either of these were not valid, nothing would happen.
    if(sName == "" ||
       !GetIsObjectValid(oRunningOn))
        return;

    // Remove the thread variables
    DeleteLocalInt(oRunningOn,    PREFIX + sName);
    DeleteLocalString(oRunningOn, PREFIX + sName + SUFFIX_SCRIPT);
    DeleteLocalFloat(oRunningOn,  PREFIX + sName + SUFFIX_INTERVAL);

    // Increase the iteration so that any lingering runthread fail to fire if the thread is restarted
    int iExpectedIteration = GetLocalInt(oRunningOn, PREFIX + sName + SUFFIX_ITERATION);
    iExpectedIteration++;
    SetLocalInt(oRunningOn, PREFIX + sName + SUFFIX_ITERATION, iExpectedIteration);
}


void TerminateCurrentThread(){
    TerminateThread(GetLocalString(GetModule(), PREFIX + CUR_THREAD),
                    GetLocalObject(GetModule(), PREFIX + CUR_THREAD)
    );
}


int SleepThread(string sName, object oRunningOn = OBJECT_INVALID){
    if(oRunningOn == OBJECT_INVALID)
        oRunningOn = GetModule();

    // Check paramaeters for correctness
    if(sName == "" ||
       !GetIsObjectValid(oRunningOn))
        return FALSE;

    // Change thread state
    SetLocalInt(oRunningOn, PREFIX + sName, THREAD_STATE_SLEEPING);

    return TRUE;
}


int AwakenThread(string sName, object oRunningOn = OBJECT_INVALID){
    if(oRunningOn == OBJECT_INVALID)
        oRunningOn = GetModule();

    // Check paramaeters for correctness
    if(sName == "" ||
       !GetIsObjectValid(oRunningOn))
        return FALSE;

    // Change thread state
    SetLocalInt(oRunningOn, PREFIX + sName, THREAD_STATE_RUNNING);

    return TRUE;
}


int ChangeExecutionInterval(string sName, float fNewInterval, object oRunningOn = OBJECT_INVALID){
    if(oRunningOn == OBJECT_INVALID)
        oRunningOn = GetModule();

    // Check paramaeters for correctness
    if(!GetThreadState(sName, oRunningOn) ||
       fNewInterval <= 0.0f)
        return FALSE;


    SetLocalFloat(oRunningOn, PREFIX + sName + SUFFIX_INTERVAL, fNewInterval);
    return TRUE;
}


void RunThread(string sName, object oRunningOn, int iIteration){
    // Abort if we're on the wrong iteration, this allows us to
    // be liberal about spawning threads in case they've timed
    // out while a PC was logged out
    int iExpectedIteration = GetLocalInt(oRunningOn, PREFIX + sName + SUFFIX_ITERATION);
    if(iIteration != iExpectedIteration)
        return;
    iExpectedIteration++;
    SetLocalInt(oRunningOn, PREFIX + sName + SUFFIX_ITERATION, iExpectedIteration);

    // Abort if the object we're running on has ceased to exist
    // or if the thread has been terminated
    int nThreadState = GetThreadState(sName, oRunningOn);
    if(nThreadState == THREAD_STATE_DEAD)
        return;

    // Mark this thread as running
    SetLocalString(GetModule(), PREFIX + CUR_THREAD, sName);
    SetLocalObject(GetModule(), PREFIX + CUR_THREAD, oRunningOn);

    // Execute the threadscript if the thread is running atm
    if(nThreadState == THREAD_STATE_RUNNING){
        string sScript = GetLocalString(oRunningOn, PREFIX + sName + SUFFIX_SCRIPT);
        ExecuteScript(sScript, oRunningOn);
    }

    // Schedule next execution, unless we've been terminated
    if(GetThreadState(sName, oRunningOn) != THREAD_STATE_DEAD){
        DelayCommand(GetLocalFloat(oRunningOn, PREFIX + sName + SUFFIX_INTERVAL), RunThread(sName, oRunningOn, iExpectedIteration));
    }

    // Clean up the module variables
    DeleteLocalString(GetModule(), PREFIX + CUR_THREAD);
    DeleteLocalObject(GetModule(), PREFIX + CUR_THREAD);
}


// Test main
//void main(){}