/** \file
 *
 *  Contains the LuaAPI class implementation.
 *
 *  Copyright (c) 2007,2008,2009 MBARI
 *  MBARI Proprietary Information.  All Rights Reserved
 *
 */

#include "LuaAPI.h"

#include "data/DataReader.h"
#include "data/DataWriter.h"
#include "lua/LuaNode.h"
#include "missionScript/DefineBehavior.h"
#include "missionScript/Method.h"
#include "utils/AuvMath.h"
#include "../../Lib/lua/lua.hpp"

LuaAPI LuaAPI::Instance_;

LuaAPI::~LuaAPI()
{
    delete rootNode_;
}

LuaAPI::LuaState::LuaState()
{
    lua_ = lua_open();
    luaL_openlibs( lua_ );
}

LuaAPI::LuaState::~LuaState()
{
    lua_close( lua_ );
}

LuaAPI::LuaState LuaAPI::LUA_;

/// loads the indicated file.
/// Returns pointer to error message if unable to do so.
const char* LuaAPI::LoadFile( const char* filename )
{
    int lua_Status = luaL_loadfile( LUA_, filename );

    if( lua_Status != 0 )
    {
        const char* errMessage = lua_tostring( LUA_, -1 );
        lua_pop( LUA_, 1 );  // remove error message
        return errMessage;
    }
    return NULL;
}

ScriptNode* LuaAPI::makeScriptFunction( const char* moduleName,
                                        const char* componentName, const char* methodName,
                                        FlexArray<InputInfo*>& inputs, FlexArray<SettingInfo*>& settings,
                                        const char* code, Logger& logger )
{
    LuaNode* moduleNode;
    if( moduleName == NULL )
    {
        moduleNode = rootNode_;
    }
    else
    {
        moduleNode = rootNode_->getNamedNode( moduleName );
        if( NULL == moduleNode )
        {
            moduleNode = rootNode_->newChildNode( moduleName );
        }
    }
    LuaNode* componentNode = moduleNode->getNamedNode( componentName );
    if( NULL == componentNode )
    {
        componentNode = moduleNode->newChildNode( componentName );
    }

    Str returnsFunction( "function(" );
    bool needsComma = false;
    for( unsigned int i = 0; i < inputs.size(); ++i )
    {
        if( needsComma )
        {
            returnsFunction += ", ";
        }
        returnsFunction += inputs[i]->getName();
        needsComma = true;
    }
    for( unsigned int i = 0; i < settings.size(); ++i )
    {
        if( needsComma )
        {
            returnsFunction += ", ";
        }
        returnsFunction += settings[i]->getName();
        needsComma = true;
    }
    returnsFunction += ")\n";
    returnsFunction += code;
    returnsFunction += "\nend;";

    return componentNode->newChildFunction( methodName, returnsFunction, logger );
}

/// Runs the specified command, with no return.
/// Returns pointer to error message if unable to do so.
const char* LuaAPI::Exec( const char* command )
{
    // execute Lua program
    luaL_loadstring( LUA_, command );

    int lua_Status = lua_pcall( LUA_, 0, 0, 0 );

    if( lua_Status != 0 )
    {
        const char* errMessage = lua_tostring( LUA_, -1 );
        lua_pop( LUA_, 1 );  // remove error message
        return errMessage;
    }
    return NULL;
}

/// Runs the function on top of the stack with the indicated args
/// and sets the indicated outputs. Returns true on success.
bool LuaAPI::Exec( FlexArray<DataReader*>* inputs, FlexArray<SettingReader*>* settings,
                   bool& boolValue, FlexArray<DataWriter*>* outputs, FlexArray<const Unit*>* outUnits,
                   Logger& logger )
{
    int top = lua_gettop( LUA_ ) - 1;

    unsigned int inputCount = NULL == inputs ? 0 : inputs->size();
    for( unsigned int i = 0; i < inputCount; ++i )
    {
        double value;
        const Unit* defaultUnit = inputs->get( i )->getDefaultUnit();
        if( NULL == defaultUnit )
        {
            defaultUnit = &Units::NONE;
        }
        inputs->get( i )->read( *defaultUnit, value );
        lua_pushnumber( LUA_, value );
    }

    unsigned int settingCount = NULL == settings ? 0 : settings->size();
    for( unsigned int i = 0; i < settingCount; ++i )
    {
        double value;
        const Unit* defaultUnit = settings->get( i )->getDefaultUnit();
        if( NULL == defaultUnit )
        {
            defaultUnit = &Units::NONE;
        }
        settings->get( i )->read( *defaultUnit, value );
        lua_pushnumber( LUA_, value );
    }

    int outputCount;
    if( NULL == outputs || NULL == outUnits )
    {
        outputCount = 0;
    }
    else
    {
        outputCount = AuvMath::Min<unsigned int>( outputs->size(), outUnits->size() );
    }

    int lua_Status = lua_pcall( LUA_, inputCount + settingCount, LUA_MULTRET, 0 );
    if( lua_Status != 0 )
    {
        const char* errMessage = lua_tostring( LUA_, -1 );
        logger.syslog( "Error running lua code: ", errMessage, Syslog::CRITICAL );
        lua_pop( LUA_, 1 );  // remove error message
        return false;
    }

    int returnCount = lua_gettop( LUA_ ) - top;

    int severity = returnCount > 1 && lua_isnumber( LUA_, -1 ) ? lua_tointeger( LUA_, -1 ) : 0;
    if( !severity && !lua_isnumber( LUA_, -1 ) && lua_isstring( LUA_, -1 ) )
    {
        const char* severityStr = lua_tostring( LUA_, -1 );
        severity = Syslog::StringToSeverity( severityStr );
    }
    const char* syslogMsg = returnCount > 1 && !lua_isnumber( LUA_, -2 ) && lua_isstring( LUA_, -2 ) ? lua_tostring( LUA_, -2 ) : NULL;
    if( NULL != syslogMsg )
    {
        logger.syslog( syslogMsg, ( Syslog::Severity )severity );
    }

    int loopCount = returnCount - ( NULL != syslogMsg ? 2 : 0 );
    for( int i = 0; i < loopCount && i < outputCount; ++i )
    {
        int stackPos = i - returnCount;
        if( !lua_isnil( LUA_, stackPos ) )
        {
            double value = lua_isnumber( LUA_, stackPos ) ? lua_tonumber( LUA_, stackPos ) : nan( "" );
            DataWriter* writer = outputs->get( i );
            if( NULL != writer )
            {
                writer->write( *outUnits->get( i ), value );
            }
        }
    }
    if( boolValue )
    {
        boolValue = lua_toboolean( LUA_, -returnCount );
    }
    lua_pop( LUA_, returnCount ); // remove results

    return true;
}

/// Runs whatever is on top of the stack with no args and no return.
/// Returns pointer to error message if unable to do so.
const char* LuaAPI::Run()
{
    // execute Lua program
    int lua_Status = lua_pcall( LUA_, 0, LUA_MULTRET, 0 );

    if( lua_Status != 0 )
    {
        const char* errMessage = lua_tostring( LUA_, -1 );
        lua_pop( LUA_, 1 );  // remove error message
        return errMessage;
    }
    return NULL;
}

/// Runs specified function from the currently open table.
/// Returns pointer to error message if unable to do so.
const char* LuaAPI::RunFunction( const char* functionName, Str& returnValue, bool isRoot )
{
    lua_getfield( LUA_, isRoot ? LUA_GLOBALSINDEX : -1, functionName );
    if( lua_isnil( LUA_, -1 ) )
    {
        lua_pop( LUA_, 1 );  // remove nil
        return "Function name not found";
    }
    int lua_Status = lua_pcall( LUA_, 0, 1, 0 ); // 0 args, 1 return

    if( lua_Status != 0 )
    {
        const char* errMessage = lua_tostring( LUA_, -1 );
        lua_pop( LUA_, 1 );  // remove error message
        return errMessage;
    }

    if( !GetString( returnValue, false ) )
    {
        return "Function must return a string.";
    }

    return NULL;
}

/// Is the item at the top of the stack a table?
bool LuaAPI::IsTable( bool isRoot )
{
    if( isRoot )
    {
        return true;
    }
    bool isTable = lua_istable( LUA_, -1 );
    return isTable;
}

/// opens an item from the table on the top of the stack by its zero-based index
/// it must be closed later with CloseTable()
bool LuaAPI::OpenTableItemByIndex( int index, bool isRoot )
{
    //printf("LuaAPI::OpenTableItemByIndex( %d, %s ) (%d)\n", index, isRoot ? "true" : "false", GetStackHeight());
    bool opened( false );
    if( IsTable( isRoot ) )
    {
        lua_rawgeti( LUA_, isRoot ? LUA_GLOBALSINDEX : -1, index + 1 );
        opened = !lua_isnil( LUA_, -1 );
    }
    else
    {
        PushNil();
    }
    //printf("opened = %s\n", opened ? "true" : "false" );
    return opened;
}

/// opens an item from the table on the top of the stack by its name
/// it must be closed later with CloseTable()
bool LuaAPI::OpenTableItemByName( const char* name, bool isRoot )
{
    //printf("LuaAPI::OpenTableItemByName( %s, %s ) (%d)\n", name, isRoot ? "true" : "false", GetStackHeight());
    bool opened( false );
    if( IsTable( isRoot ) )
    {
        lua_getfield( LUA_, isRoot ? LUA_GLOBALSINDEX : -1, name );
        opened = !lua_isnil( LUA_, -1 );
    }
    else
    {
        PushNil();
    }
    //printf("opened = %s\n", opened ? "true" : "false" );
    return opened;
}

/// Pushes a nil onto the stack
void LuaAPI::PushNil()
{
    lua_pushnil( LUA_ );
}

/// returns the height of the stack
int LuaAPI::GetStackHeight()
{
    return lua_gettop( LUA_ );
}

/// Returns the size of the table on the top of the stack
/// Returns -1 if not a table.
int LuaAPI::GetTableSize( bool isRoot )
{
    if( IsTable( isRoot ) )
    {
        return lua_objlen( LUA_, isRoot ? LUA_GLOBALSINDEX : -1 );
    }
    return -1;
}

/// Just pops the top item off the LUA_ stack.
void LuaAPI::CloseTableItem()
{
    //printf("In closeTableItem with stack height = %d\n", GetStackHeight());
    lua_pop( LUA_, 1 );
}

/// gets a boolean from the top of the stack
bool LuaAPI::GetBool( bool& readTo, bool isRoot )
{
    readTo = lua_toboolean( LUA_, isRoot ? LUA_GLOBALSINDEX : -1 );
    return lua_isboolean( LUA_, isRoot ? LUA_GLOBALSINDEX : -1 );
}

/// gets a number from the top of the stack
bool LuaAPI::GetNumber( double& readTo, bool isRoot )
{
    if( !lua_isnumber( LUA_, isRoot ? LUA_GLOBALSINDEX : -1 ) )
    {
        return false;
    }
    readTo = lua_tonumber( LUA_, isRoot ? LUA_GLOBALSINDEX : -1 );
    return true;
}

/// gets a unsigned char string from the top of the stack
bool LuaAPI::GetString( Str& readTo, bool isRoot )
{
    if( lua_type( LUA_, isRoot ? LUA_GLOBALSINDEX : -1 ) != LUA_TSTRING )   // lua_isstring returns true for numbers sometimes
    {
        return false;
    }
    readTo = lua_tolstring( LUA_, isRoot ? LUA_GLOBALSINDEX : -1, NULL );
    return true;

}

/// creates a new empty table and adds to open node, returns true if successful
bool LuaAPI::NewTable( const char* name, bool isRoot )
{
    if( !isRoot && lua_type( LUA_, -1 ) != LUA_TTABLE )
    {
        return false;
    }
    lua_pushstring( LUA_, name );// Stack = 1
    lua_newtable( LUA_ ); // Stack = 2
    lua_rawset( LUA_, isRoot ? LUA_GLOBALSINDEX : -3 ); // Stack = 0
    return true;
}

/// creates a new function and adds to open node, returns error message if error
const char* LuaAPI::NewFunction( const char* functionName, const char* functionBody, bool isRoot )
{
    // Enable global lookups.
    if( !lua_getmetatable( LUA_, -1 ) )
    {
        lua_createtable( LUA_, 0, 1 ); /* create new metatable */
        lua_pushvalue( LUA_, -1 );
        lua_setmetatable( LUA_, -3 );
        lua_pushvalue( LUA_, LUA_GLOBALSINDEX );
        lua_setfield( LUA_, -2, "__index" );  /* mt.__index = _G */
    }
    CloseTableItem();

    if( !isRoot && lua_type( LUA_, -1 ) != LUA_TTABLE )
    {
        return "Can only add a function to a table node.";
    }

    lua_pushstring( LUA_, functionName );// Stack = 1

    Str returnsFunction( "return " );
    returnsFunction += functionBody;
    int lua_Status = luaL_loadstring( LUA_, returnsFunction.cStr() ); // Stack = 2
    if( lua_Status != 0 )
    {
        const char* errMessage = lua_tostring( LUA_, -1 );
        lua_pop( LUA_, 2 );  // remove name + error message, Stack = 0
        return errMessage;
    }

    lua_Status = lua_pcall( LUA_, 0, 1, 0 ); // Stack = 2
    if( lua_Status != 0 )
    {
        const char* errMessage = lua_tostring( LUA_, -1 );
        lua_pop( LUA_, 2 );  // remove name + error message, Stack = 0
        return errMessage;
    }

    // Copy the table node to the top of the stack
    lua_pushvalue( LUA_, -3 ); // Stack = 3

    // Set the table as the environment for the function
    lua_setfenv( LUA_, -2 ); // Stack = 2

    // Set the function as a member of the table
    lua_rawset( LUA_, isRoot ? LUA_GLOBALSINDEX : -3 ); // Stack = 0

    return NULL;
}

LuaAPI::LuaAPI()
    : ScriptAPI( "Lua", ".lua" ),
      rootNode_( new LuaNode() )
{}

