/*****************************************************************************
 * $LastChangedDate: 2011-08-20 12:07:38 -0400 (Sat, 20 Aug 2011) $
 * @file
 * @author  Jim E. Brooks  http://www.palomino3d.org
 * @brief   Interface for Lua scripts.
 * @remarks - module.txt documents how to export new C++ classes to Lua.
 *          - Lua's C interface is based on a virtual stack.
 *          - Top-of-stack is referenced by -1.
 *          - When execution transitions from C++ to Lua,
 *            the C/C++ side must not overflow Lua's virtual stack,
 *            so Lua::SaveStack()/RestoreStack()/ThrowException()
 *            are used in methods that may affect Lua's stack.
 *          - When execution transitions from Lua to C/C++,
 *            Lua will automatically restore the virtual stack.
 *            But tracking stack items is still necessary while writing C++,
 *            since the C++ side usually has to return results to Lua via the stack.
 *          - Lua treats table members referenced by a numeric index (array)
 *            or string key (dictionary) separately.
 *            Thus, lua_rawgeti() or lua_getfield() must be used, resp.
 *          - lua_isnumber() etc will automatically convert between numbers and strings.
 *          - The Lua interpreter can be run in (only one) separate thread.
 *            Once that thread is spawned, the main thread cannot ever access Lua again,
 *            as two threads cannot make function calls into each other.
 *//*
 * LEGAL:   COPYRIGHT (C) 2008 JIM E. BROOKS
 *          THIS SOURCE CODE IS RELEASED UNDER THE TERMS
 *          OF THE GNU GENERAL PUBLIC LICENSE VERSION 2 (GPL 2).
 *****************************************************************************/

// Avoid lua_/LUA_ which is reserved by Lua.

#include <utility>
#include <vector>
#include "base/module.hh"
#include "base/singleton.hh"
#include "base/file.hh"
#include "base/stream.hh"
using namespace base;
#include "lua_bind/module.hh"
#include "lua_bind/lua.hh"
#include "lua_bind/bind.hh"

//==============================================================================

#if ! DOXYGEN
// Main functions to export C++ functions/classes to Lua.
// Expand macros as Register*() function prototypes.
namespace lua {
namespace bind {
#undef  REG_FUNC_BEGIN
#undef  REG_FUNC_ARGS
#undef  REG_FUNC_END
#undef  REG_FUNC_CMDLINE
#define REG_FUNC_BEGIN   void
#define REG_FUNC_ARGS    lua_State* L
#define REG_FUNC_END     ;
#define REG_FUNC_CMDLINE void RegisterCmdLine( lua_State* L, int argc, char** argv );
#include "lua_bind/register.hh"
} // namespace bind
} // namespace lua
using namespace lua::bind;
#endif // ! DOXYGEN

//==============================================================================

namespace lua {
namespace interpreter {

////////////////////////////////////////////////////////////////////////////////
/////////////////////////////////  Lua  ////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////

DEFINE_SINGLETON_CLASS_VARS( Lua )
bool Lua::msTypeErrorAsException = false;

/*****************************************************************************
 * ctor/dtor.
 *****************************************************************************/
Lua::Lua( void )
:   mLua(lua_open()),       // create Lua interpreter
    mScriptDir(LUABIND_SCRIPTS_DIR),  // default
    mBytecodeMap(),
    mSavedStack(0)
{
    luaL_openlibs( mLua );

//------------------------------------------------------------------------------
// Expand macros as Register*() function calls.
// Register with Lua all C++ function and class interfaces.
#undef  REG_FUNC_BEGIN
#undef  REG_FUNC_ARGS
#undef  REG_FUNC_END
#undef  REG_FUNC_CMDLINE
#define REG_FUNC_BEGIN
#define REG_FUNC_ARGS    mLua
#define REG_FUNC_END     ;
#define REG_FUNC_CMDLINE // omit calling RegisterCmdLine()
#include "lua_bind/register.hh"
//------------------------------------------------------------------------------

}

Lua::~Lua()
{
    lua_close( mLua );
    mLua = NULL;
}

/*****************************************************************************
 * Export command-line args to Lua.
 *****************************************************************************/
void
Lua::ExportCmdLine( int argc, char** argv )
{
 BEGIN_ONCE {
    RegisterCmdLine( mLua, argc, argv );
 } END_ONCE
}

/*****************************************************************************
 * Set the directory containing Lua scripts.
 *****************************************************************************/
void
Lua::SetScriptDir( const string& scriptDir )
{
    mScriptDir = scriptDir;

    // Does dir exist?
    if ( not IfDirExists( mScriptDir ) )
        THROW_LUA_EXCEPTION( "[Lua::SetScriptDir]: dir doesn't exist" );

    // Flush cache in case another dir has scripts with the same name.
    mBytecodeMap.clear();
}

/*****************************************************************************
 * THROWS C++ EXCEPTION.
 * Execute a script.  To avoid recompiling, the byte-code of a Lua script
 * is stored in case the same script is called again.
 * http://lua-users.org/lists/lua-l/2003-07/msg00082.html
 *****************************************************************************/
void
Lua::RunScript( const string& scriptName, const bool recompile ) // throw
{
ASSERT( not mScriptDir.empty() );
ASSERT( not scriptName.empty() );

SaveStack();

    // Has this script been compiled as byte-code before?
    BytecodeMap::iterator iter = mBytecodeMap.find( scriptName );
    if ( UX( iter == mBytecodeMap.end() or recompile ) )
    {
        // No, this script hasn't been compiled.

        // Prepend script directory.
        const string filename = mScriptDir + string("/") + scriptName;

        #if LUABIND_VERBOSE
        CLOG << "[Lua::RunScript] compiling " << scriptName << std::endl;
        #endif

        if ( UX( not IfFileExists( filename ) ) )
            ThrowLuaException( string("[Lua::RunScript]: script not found: ") + filename );

        // "luaL_loadfile() loads a Lua chunk.  If there are no errors,
        // lua_load pushes the compiled chunk as a Lua function on top of the stack."
        if ( UX( luaL_loadfile( mLua, filename.c_str() ) != 0 ) )
            ThrowLuaException( string("[Lua::RunScript]: ") + string(lua_tostring(mLua,-1)) );

        // Cache the byte-code in a STL map.
        // luaL_loadfile() has placed byte-code on the stack.
        // Store the byte-code in the Lua Registry and into a C++ cache (STL map).
        // "luaL_ref(): Creates and returns a reference, in the table at index t,
        //  for the object at the top of the stack (and pops the object)."
        const int luaFuncRef = luaL_ref( mLua, LUA_REGISTRYINDEX );
        if ( luaFuncRef == LUA_REFNIL )
            ThrowLuaException( string("[Lua::RunScript]: LUA_REFNIL") );
        mBytecodeMap.insert( std::make_pair<string,int>( scriptName, luaFuncRef ) );

        // Push it back since luaL_ref() popped it.
        lua_rawgeti( mLua, LUA_REGISTRYINDEX, luaFuncRef );
    }
    else
    {
        #if LUABIND_VERBOSE
        CLOG << "[Lua::RunScript] re-executing pre-compiled " << scriptName << std::endl;
        #endif

        // Push the pre-compiled byte-code.
        const int luaFuncRef = iter->second;
        lua_rawgeti( mLua, LUA_REGISTRYINDEX, luaFuncRef );
    }

    // Execute the Lua byte-code that was pushed on the stack.
    if ( UX( lua_pcall( mLua, 0, 0, 0 ) ) )
        ThrowLuaException( string("[Lua::RunScript]: ") + string(lua_tostring(mLua,-1)) );

RestoreStack();
}

/*****************************************************************************
 * THROWS C++ EXCEPTION.
 * Read a boolean variable from Lua.
 * Note that the Lua C API treats boolean and number as distinct types.
 * But this method will treat any non-zero number as "true" (as in C++).
 * So this method will treat these as equivalent:
 * enableLighting = true
 * enableLighting = 1
 *****************************************************************************/
bool
Lua::ReadVarAsBool( const string& varName )  // throw
{
SaveStack();
    lua_getglobal( mLua, varName.c_str() );
    bool val = false;
    if ( lua_isboolean( mLua, -1 ) )       // boolean type?
        val = BOOL_CAST( lua_toboolean( mLua, -1 ) );
    else if ( IF_NUMBER( mLua, -1 ) )  // number?
        val = (lua_tonumber( mLua, -1 ) != 0.0);
    else
        ThrowLuaException( "[Lua::ReadVarAsBool] not a boolean/number" );
RestoreStack();
    return val;
}

/*****************************************************************************
 * THROWS C++ EXCEPTION.
 * Read a number variable from Lua.
 *****************************************************************************/
LuaNumber
Lua::ReadVarAsNumber( const string& varName )  // throw
{
SaveStack();
    lua_getglobal( mLua, varName.c_str() );
    if ( not IF_NUMBER( mLua, -1 ) )
        ThrowLuaException( "[Lua::ReadVarAsNumber] not a number" );
    LuaNumber val = lua_tonumber( mLua, -1 );
RestoreStack();
    return val;
}

/*****************************************************************************
 * THROWS C++ EXCEPTION.
 * Read a string variable from Lua.
 *****************************************************************************/
string
Lua::ReadVarAsString( const string& varName )  // throw
{
SaveStack();
    lua_getglobal( mLua, varName.c_str() );
    if ( not IF_STRING( mLua, -1 ) )
        ThrowLuaException( "[Lua::ReadVarAsString] not a string" );
    string val = lua_tostring( mLua, -1 );
RestoreStack();
    return val;
}

/*****************************************************************************
 * THROWS C++ EXCEPTION.
 * Read a member of table by numeric index (treats table as an array).
 *****************************************************************************/
LuaNumber
Lua::ReadTableAsNumber( const string& tableName, const int tableIdx )  // throw
{
SaveStack();

    // Push table.
    lua_getglobal( mLua, tableName.c_str() );
    if ( not lua_istable( mLua, -1 ) )
        ThrowLuaException( "[Lua::ReadTableAsNumber] not a table" );

    // Push value of element.
    lua_rawgeti( mLua, -1, tableIdx );

    // Is it a number?
    if ( not IF_NUMBER( mLua, -1 ) )
        ThrowLuaException( "[Lua::ReadTableAsNumber] member isn't a number" );

    LuaNumber val = lua_tonumber( mLua, -1 );
RestoreStack();
    return val;
}

/*****************************************************************************
 * THROWS C++ EXCEPTION.
 * Read a member of table by key (key is a string).
 *****************************************************************************/
LuaNumber
Lua::ReadTableAsNumber( const string& tableName, const string& memberName )  // throw
{
SaveStack();

    // Push table.
    lua_getglobal( mLua, tableName.c_str() );
    if ( not lua_istable( mLua, -1 ) )
        ThrowLuaException( "[Lua::ReadTableAsNumber] not a table" );

    // Push value of element.
    lua_getfield( mLua, -1, memberName.c_str() );

    // Is it a number?
    if ( not IF_NUMBER( mLua, -1 ) )
        ThrowLuaException( "[Lua::ReadTableAsNumber] member isn't a number" );

    LuaNumber val = lua_tonumber( mLua, -1 );
RestoreStack();
    return val;
}

/*****************************************************************************
 * THROWS C++ EXCEPTION.
 * Read a member of table by numeric index (treats table as an array).
 *****************************************************************************/
string
Lua::ReadTableAsString( const string& tableName, const int tableIdx )  // throw
{
SaveStack();

    // Push table.
    lua_getglobal( mLua, tableName.c_str() );
    if ( not lua_istable( mLua, -1 ) )
        ThrowLuaException( "[Lua::ReadTableAsString] not a table" );

    // Push value of element.
    lua_rawgeti( mLua, -1, tableIdx );

    // Is it a string?
    if ( not IF_STRING( mLua, -1 ) )
        ThrowLuaException( "[Lua::ReadTableAsString] member isn't a string" );

    string val = lua_tostring( mLua, -1 );
RestoreStack();
    return val;
}

/*****************************************************************************
 * THROWS C++ EXCEPTION.
 * Read a member of table by key (key is a string).
 *****************************************************************************/
string
Lua::ReadTableAsString( const string& tableName, const string& memberName )  // throw
{
SaveStack();

    // Push table.
    lua_getglobal( mLua, tableName.c_str() );
    if ( not lua_istable( mLua, -1 ) )
        ThrowLuaException( "[Lua::ReadTableAsString] not a table" );

    // Push value of element.
    lua_getfield( mLua, -1, memberName.c_str() );

    // Is it a string?
    if ( not IF_STRING( mLua, -1 ) )
        ThrowLuaException( "[Lua::ReadTableAsString] member isn't a string" );

    string val = lua_tostring( mLua, -1 );
RestoreStack();
    return val;
}

/*****************************************************************************
 * Call a Lua function from C++ (as a method in a table).
 * tableName:funcName( args )
 * @param   tableName
 * @param   funcName
 * @param   args
 *          Can be empty.
 *****************************************************************************/
LuaNumber
Lua::CallLuaFunc( const string& tableName, const string& funcName, const std::vector<LuaNumber>& args )
{
ASSERT( args.size() < defs::MAX_LUA_ARGS );  // allow empty args

    // Push Lua function then its args.
    lua_getglobal( mLua, tableName.c_str() );       // push table
    lua_getfield( mLua, -1, funcName.c_str() );     // push method of table
    for ( std::vector<LuaNumber>::const_reverse_iterator iter = args.rbegin();
          iter != args.rend();
          ++iter )  // yes, ++
    {
        lua_pushnumber( mLua, *iter );              // push arg to method
    }

    // Call Lua function.
    if ( UX( lua_pcall( mLua, args.size(), 1, 0 ) != 0 ) )
    {
        string msg = string("[Lua::CallLuaFunc] ");
        msg += tableName; msg += ":"; msg += funcName; msg += "(): ";
        msg += lua_tostring( mLua, -1 );
        ThrowLuaException( msg );
    }

    // Return result from Lua function.
    if ( not lua_isnumber( mLua, -1 ) )
        ThrowLuaException( "[Lua::CallLuaFunc] result error" );
    const LuaNumber result = lua_tonumber( mLua, -1 );
    lua_pop( mLua, 1 );
    return result;
}

} // namespace interpreter
} // namespace lua
