/*
    _____               __  ___          __            ____        _      __
   / ___/__ ___ _  ___ /  |/  /__  ___  / /_____ __ __/ __/_______(_)__  / /_
  / (_ / _ `/  ' \/ -_) /|_/ / _ \/ _ \/  '_/ -_) // /\ \/ __/ __/ / _ \/ __/
  \___/\_,_/_/_/_/\__/_/  /_/\___/_//_/_/\_\\__/\_, /___/\__/_/ /_/ .__/\__/
                                               /___/             /_/
                                             
  See Copyright Notice in gmMachine.h

*/

#ifndef _GMCALL_H_
#define _GMCALL_H_

#include "gmConfig.h"
#include "gmThread.h"
#include "gmMachine.h"

#undef GetObject //Fix for Win32 where GetObject is #defined

/// \class gmCall
/// \brief A helper class to call script functions from C
/// Warning: Do not store any of the reference type return variables (eg. GM_SRING).  
/// As the object may be garbage collected.  Instead, copy immediately as necessary.
class gmCall
{
public:

  /// \brief Constructor
  gmCall()
  {
#ifdef GM_DEBUG_BUILD
    m_locked = false;
#endif //GM_DEBUG_BUILD
  }

  /// \brief Begin the call of a global function
  /// \param a_machine Virtual machine instance 
  /// \param a_funcName Name of function
  /// \param a_this The 'this' used by the function.
  /// \param a_delayExecuteFlag Set true if you want function thread to not execute now.
  /// \return true on sucess, false if function was not found.
  GM_FORCEINLINE bool BeginGlobalFunction(gmMachine * a_machine, const char * a_funcName, 
                                          const gmVariable& a_this = gmVariable::s_null, 
                                          bool a_delayExecuteFlag = false)
  {
    GM_ASSERT(a_machine);

    gmStringObject * funcNameStringObj = a_machine->AllocPermanantStringObject(a_funcName); // Slow
    
    return BeginGlobalFunction(a_machine, funcNameStringObj, a_this, a_delayExecuteFlag);
  }

  /// \brief Begin the call of a global function
  /// \param a_machine Virtual machine instance 
  /// \param a_funcNameStringObj A string object that was found or created earlier, much faster than creating from c string.
  /// \param a_this The 'this' used by the function.
  /// \param a_delayExecuteFlag Set true if you want function thread to not execute now.
  /// \return true on sucess, false if function was not found.
  GM_FORCEINLINE bool BeginGlobalFunction(gmMachine * a_machine, gmStringObject * a_funcNameStringObj, 
                                          const gmVariable& a_this = gmVariable::s_null, 
                                          bool a_delayExecuteFlag = false)
  {
    GM_ASSERT(a_machine);
    GM_ASSERT(a_funcNameStringObj);
    
    gmVariable lookUpVar;
    gmVariable foundFunc;

    lookUpVar.SetString(a_funcNameStringObj);
    foundFunc = a_machine->GetGlobals()->Get(lookUpVar);

    if( GM_FUNCTION == foundFunc.m_type )         // Check found variable is a function
    {
      gmFunctionObject * functionObj = (gmFunctionObject *)foundFunc.m_value.m_ref; //Func Obj from variable
  
      return BeginFunction(a_machine, functionObj, a_this, a_delayExecuteFlag);
    }
    return false;
  }

  /// \brief Begin the call of a object function
  /// \param a_machine Virtual machine instance 
  /// \param a_funcName Name of function.
  /// \param a_tableObj The table on the object to look up the function.
  /// \param a_this The 'this' used by the function.
  /// \param a_delayExecuteFlag Set true if you want function thread to not execute now.
  /// \return true on sucess, false if function was not found.
  GM_FORCEINLINE bool BeginTableFunction(gmMachine * a_machine, const char * a_funcName, 
                                  gmTableObject * a_tableObj, const gmVariable& a_this = gmVariable::s_null, 
                                  bool a_delayExecuteFlag = false)
  {
    GM_ASSERT(a_machine);
    GM_ASSERT(a_funcName);

    gmStringObject * funcNameStringObj = a_machine->AllocPermanantStringObject(a_funcName); // Slow
    
    return BeginTableFunction(a_machine, funcNameStringObj, a_tableObj, a_this, a_delayExecuteFlag);
  }

  /// \brief Begin the call of a object function
  /// \param a_machine Virtual machine instance 
  /// \param a_funcNameStringObj A string object that was found or created earlier, much faster than creating from c string.
  /// \param a_tableObj The table on the object to look up the function.
  /// \param a_this The 'this' used by the function.
  /// \param a_delayExecuteFlag Set true if you want function thread to not execute now.
  /// \return true on sucess, false if function was not found.
  GM_FORCEINLINE bool BeginTableFunction(gmMachine * a_machine, gmStringObject * a_funcNameStringObj, 
                                  gmTableObject * a_tableObj, const gmVariable& a_this = gmVariable::s_null, 
                                  bool a_delayExecuteFlag = false)
  {
    GM_ASSERT(a_machine);   
    GM_ASSERT(a_tableObj);
    GM_ASSERT(a_funcNameStringObj);

    gmVariable lookUpVar;
    gmVariable foundFunc;

    lookUpVar.SetString(a_funcNameStringObj);
    foundFunc = a_tableObj->Get(lookUpVar);

    if( GM_FUNCTION == foundFunc.m_type )         // Check found variable is a function
    {
      gmFunctionObject * functionObj = (gmFunctionObject *)foundFunc.m_value.m_ref; //Func Obj from variable
      return BeginFunction(a_machine, functionObj, a_this, a_delayExecuteFlag);
    }
    return false;
  }

  /// \brief Begin the call of a object function
  /// \param a_funcObj A function object that was found or created earlier.
  /// \param a_tableObj The table on the object to look up the function.
  /// \param a_this The 'this' used by the function.
  /// \param a_delayExecuteFlag Set true if you want function thread to not execute now.
  /// \return true on sucess, false if function was not found.
  GM_FORCEINLINE bool BeginFunction(gmMachine * a_machine, gmFunctionObject * a_funcObj, 
                                    const gmVariable &a_thisVar = gmVariable::s_null, 
                                    bool a_delayExecuteFlag = false)
  {
    GM_ASSERT(a_machine);   
    GM_ASSERT(a_funcObj);

#ifdef GM_DEBUG_BUILD
    // YOU CANNOT NEST gmCall::Begin
    GM_ASSERT(m_locked == false);
    m_locked = true;
#endif //GM_DEBUG_BUILD

    Reset(a_machine);

    if( GM_FUNCTION == a_funcObj->GetType() )         // Check found variable is a function
    {
      m_thread = m_machine->CreateThread();     // Create thread for func to run on      
      m_thread->Push(a_thisVar);                // this
      m_thread->PushFunction(a_funcObj);        // function
      m_delayExecuteFlag = a_delayExecuteFlag;
      return true;
    }

#ifdef GM_DEBUG_BUILD
    GM_ASSERT(m_locked == true);
    m_locked = false;
#endif //GM_DEBUG_BUILD

    return false;
  }
 
  /// \brief Add a parameter variable
  GM_FORCEINLINE void AddParam(const gmVariable& a_var)
  {
    GM_ASSERT(m_machine);
    GM_ASSERT(m_thread);

    m_thread->Push(a_var);
    ++m_paramCount;
  }

  /// \brief Add a parameter that is null
  GM_FORCEINLINE void AddParamNull()
  {
    GM_ASSERT(m_machine);
    GM_ASSERT(m_thread);

    m_thread->PushNull();
    ++m_paramCount;
  }

  /// \brief Add a parameter that is a integer
  GM_FORCEINLINE void AddParamInt(const int a_value)
  {
    GM_ASSERT(m_machine);
    GM_ASSERT(m_thread);

    m_thread->PushInt(a_value);
    ++m_paramCount;
  }

  /// \brief Add a parameter that is a float
  GM_FORCEINLINE void AddParamFloat(const float a_value)
  {
    GM_ASSERT(m_machine);
    GM_ASSERT(m_thread);

    m_thread->PushFloat(a_value);
    ++m_paramCount;
  }

  /// \brief Add a parameter that is a string
  GM_FORCEINLINE void AddParamString(const char * a_value, int a_len = -1)
  {
    GM_ASSERT(m_machine);    
    GM_ASSERT(m_thread);

    m_thread->PushNewString(a_value, a_len);
    ++m_paramCount;
  }

  /// \brief Add a parameter that is a string (faster version since c string does not need lookup)
  GM_FORCEINLINE void AddParamString(gmStringObject * a_value)
  {
    GM_ASSERT(m_machine);    
    GM_ASSERT(m_thread);

    m_thread->PushString(a_value);
    ++m_paramCount;
  }

  /// \brief Add a parameter that is a user object.  Creates a new user object.
  /// \param a_value Pointer to user object data
  /// \param a_userType Type of user object beyond GM_USER
  GM_FORCEINLINE void AddParamUser(void * a_value, int a_userType)
  {
    GM_ASSERT(m_machine);    
    GM_ASSERT(m_thread);

    m_thread->PushNewUser(a_value, a_userType);
    ++m_paramCount;
  }

  /// \brief Add a parameter that is a user object.
  /// \param a_userObj Pushes an existing user object without creating a new one.
  GM_FORCEINLINE void AddParamUser(gmUserObject * a_userObj)
  {
    GM_ASSERT(m_machine);
    GM_ASSERT(m_thread);

    m_thread->PushUser(a_userObj);
    ++m_paramCount;
  }

  /// \brief Add a parameter that is a table object.
  /// \param a_tableObj Pushes an existing table object without creating a new one.
  GM_FORCEINLINE void AddParamTable(gmTableObject * a_tableObj)
  {
    GM_ASSERT(m_machine);
    GM_ASSERT(m_thread);

    m_thread->PushTable(a_tableObj);
    ++m_paramCount;
  }

  /// \brief Make the call.  If a return value was expected, it will be set in here.
  /// \param a_threadId Optional 
  GM_FORCEINLINE void End(int * a_threadId = NULL)
  {
    GM_ASSERT(m_machine);
    GM_ASSERT(m_thread);
    
#ifdef GM_DEBUG_BUILD
    // CAN ONLY CALL ::End() after a successful ::Begin
    GM_ASSERT(m_locked == true);
    m_locked = false;
#endif //GM_DEBUG_BUILD
        
    int state = m_thread->PushStackFrame(m_paramCount);
    if(state != gmThread::KILLED) // Can be killed immedialy if it was a C function
    {
      if(m_delayExecuteFlag)
      {
        state = m_thread->GetState();
      }
      else
      {
        state = m_thread->Sys_Execute(&m_returnVar);
      }
    }
    else
    {
      // Was a C function call, grab return var off top of stack
      m_returnVar = *(m_thread->GetTop() - 1);
      m_machine->Sys_SwitchState(m_thread, gmThread::KILLED);
    }

    // If we requested a thread Id
    if(a_threadId)
    {
      if(state != gmThread::KILLED)
      {
        *a_threadId = m_thread->GetId();
      }
      else
      {
        *a_threadId = GM_INVALID_THREAD;
      }
    }

    if(state == gmThread::KILLED)
    {
      m_returnFlag = true; // Function always returns something, null if not explicit.
      m_thread = NULL; // Thread has exited, no need to remember it.
    }
  }

  /// \brief Accesss thread created for function call.
  GM_FORCEINLINE gmThread * GetThread() { return m_thread; }

  /// \brief Returns reference to 'return' variable.  Never fails, but variable may be a 'null' varaible if none was returned.
  const gmVariable& GetReturnedVariable() { return m_returnVar; }
  
  /// \brief Returns true if function exited and returned a variable.
  bool DidReturnVariable() { return m_returnFlag; }

  /// \brief Did function return a null?
  /// \return true if function returned null. 
  bool GetReturnedNull()
  {
    if(m_returnFlag && (m_returnVar.m_type == GM_NULL))
    {
      return true;
    }
    return false;
  }

  /// \brief Get returned int
  /// \return true if function returned an int. 
  bool GetReturnedInt(int& a_value)
  {
    if(m_returnFlag && (m_returnVar.m_type == GM_INT))
    {
      a_value = m_returnVar.m_value.m_int;
      return true;
    }
    return false;
  }

  /// \brief Get returned float
  /// \return true if function returned an float. 
  bool GetReturnedFloat(float& a_value)
  {
    if(m_returnFlag && (m_returnVar.m_type == GM_FLOAT))
    {
      a_value = m_returnVar.m_value.m_float;
      return true;
    }
    return false;
  }

  /// \brief Get returned string
  /// \return true if function returned an string. 
  bool GetReturnedString(const char *& a_value)
  {
    if(m_returnFlag && (m_returnVar.m_type == GM_STRING))
    {
      a_value = ((gmStringObject *)m_machine->GetObject(m_returnVar.m_value.m_ref))->GetString();
      return true;
    }
    return false;
  }

  /// \brief Get returned user
  /// \return true if function returned an user. 
  bool GetReturnedUser(void *& a_value, int a_userType)
  {
    if(m_returnFlag && (m_returnVar.m_type == a_userType))
    {
      a_value = (void *)m_returnVar.m_value.m_ref;
      return true;
    }
    return false;
  }

  /// \brief Get returned user or null
  /// \return true if function returned an user or null. 
  bool GetReturnedUserOrNull(void *& a_value, int a_userType)
  {
    if(m_returnFlag)
    {
      if(m_returnVar.m_type == a_userType)
      {
        a_value = (void *)m_returnVar.m_value.m_ref;
        return true;
      }
      else if(m_returnVar.m_type == GM_NULL)
      {
        a_value = (void *)NULL;
        return true;
      }
    }
    return false;
  }

protected:

  gmMachine * m_machine;
  gmThread * m_thread;
  gmVariable m_returnVar;
  int m_paramCount;
  bool m_returnFlag;
  bool m_delayExecuteFlag;
#ifdef GM_DEBUG_BUILD
  bool m_locked;
#endif //GM_DEBUG_BUILD

  /// \brief Used internally to clear call information.
  GM_FORCEINLINE void Reset(gmMachine * a_machine)
  {
    GM_ASSERT(a_machine);
    m_machine = a_machine;
    m_thread = NULL;
    m_returnVar.Nullify();
    m_returnFlag = false;
    m_paramCount = 0;
    m_delayExecuteFlag = false;
  };
 
};


#endif // _GMCALL_H_
