/******************************************************************************
    Project:    MicroMacro
    Author:     SolarStrike Software
    URL:        www.solarstrike.net
    License:    Modified BSD (see license.txt)
******************************************************************************/

#include "macro.h"
#include "logger.h"
#include "ncurses_lua.h"
#include "strl.h"
#include "debugmessages.h"

#ifdef NETWORKING_ENABLED
    #include "socket_lua.h"
#endif

extern "C"
{
#include <lua.h>
#include <lauxlib.h>
#include <lualib.h>
}

#ifdef __cplusplus
extern "C" {
#endif
BOOL WINAPI GetCurrentConsoleFont(HANDLE hConsoleOutput, BOOL bMaximumWindow, PCONSOLE_FONT_INFO lpConsoleCurrentFont);
#ifdef __cplusplus
}
#endif

using MicroMacro::Event;
using MicroMacro::EventType;
using MicroMacro::Mutex;

CMacro *CMacro::pinstance = 0;
CMacro *CMacro::instance()
{
    try {
        if( pinstance == 0 )
            pinstance = new Macro;
    }
    catch( std::bad_alloc &ba ) {
        badAllocation();
    }

    return pinstance;
}

CMacro::CMacro()
{
    appHwnd = NULL;
    appHandle = NULL;
    procId = 0;
    foregroundHwnd = NULL;
    consoleCharWidth = 0;
    consoleCharHeight = 0;
    consoleDefaultAttributes = 0;
    lastConsoleSizeX = 0;
    lastConsoleSizeY = 0;
}

CMacro::~CMacro()
{
    // Shut down Ncurses
    if( Ncurses_lua::is_initialized() )
        Ncurses_lua::cleanup(engine.getLuaState());

    engine.cleanup();
}

int CMacro::init()
{
    int success;

    success = engine.init();
    if( success != MicroMacro::ERR_OK )
        return success;

    hid.init();

    getAppHwnd();               // Cache it for later
    getAppHandle();             // ^
    getProcId();                // ^
    foregroundHwnd = appHwnd;   // Assume we're focusing this window

    // Query font info
    CONSOLE_FONT_INFO fontInfo;
    GetCurrentConsoleFont(getAppHandle()/*GetStdHandle(STD_OUTPUT_HANDLE)*/, false, &fontInfo);
    consoleCharWidth = fontInfo.dwFontSize.X;
    consoleCharHeight = fontInfo.dwFontSize.Y;

    // Get current console size
    RECT winrect;
    if( GetWindowRect(getAppHwnd(), &winrect) )
    {
        lastConsoleSizeX = winrect.right - winrect.left;
        lastConsoleSizeY = winrect.bottom - winrect.top;
    }
    else
    {
        lastConsoleSizeX = 0;
        lastConsoleSizeY = 0;
    }

    // Obtain the default attributes
    CONSOLE_SCREEN_BUFFER_INFO csbi;
    GetConsoleScreenBufferInfo(getAppHandle(), &csbi);
    consoleDefaultAttributes = csbi.wAttributes;

    return MicroMacro::ERR_OK;
}

int CMacro::cleanup()
{
    int success;
    success = engine.cleanup();
    if( success != MicroMacro::ERR_OK )
        return success;

    return MicroMacro::ERR_OK;
}

LuaEngine *CMacro::getEngine()
{
    return &engine;
}

Settings *CMacro::getSettings()
{
    return &settings;
}

Hid *CMacro::getHid()
{
    return &hid;
}

DWORD CMacro::getProcId()
{
    if( procId == 0 )
        GetWindowThreadProcessId(getAppHwnd(), &procId);

    return procId;
}

HWND CMacro::getAppHwnd()
{
    if( appHwnd == NULL ) // Automatically set
        appHwnd = GetConsoleWindow();

    return appHwnd;
}

HANDLE CMacro::getAppHandle()
{
    if( appHandle == NULL )
        appHandle = GetStdHandle(STD_OUTPUT_HANDLE);

    return appHandle;
}

void CMacro::pollForegroundWindow()
{
    HWND lastForegroundHwnd = foregroundHwnd;
    foregroundHwnd = GetForegroundWindow();

    if( foregroundHwnd != lastForegroundHwnd )
    {
        // Trigger window focus change event
        Event *pe = new Event;
        pe->type = MicroMacro::EVENT_FOCUSCHANGED;
        MicroMacro::EventData ced;
        ced.setValue((unsigned long long)foregroundHwnd);
        pe->data.push_back(ced);
        pushEvent(pe);
    }
}

void CMacro::pollConsoleResize()
{
    RECT winrect;
    if( GetWindowRect(getAppHwnd(), &winrect) )
    {
        int w = winrect.right - winrect.left;
        int h = winrect.bottom - winrect.top;
        if( w != lastConsoleSizeX ||
                h != lastConsoleSizeY )
        {
            // Update to our new size
            lastConsoleSizeX = w;
            lastConsoleSizeY = h;

            // Trigger window resize event
            Event *pe = new Event;
            pe->type = MicroMacro::EVENT_CONSOLERESIZED;
            pushEvent(pe);
        }
    }
}

HWND CMacro::getForegroundWindow()
{
    return foregroundHwnd;
}

int CMacro::getConsoleFontWidth()
{
    return consoleCharWidth;
}

int CMacro::getConsoleFontHeight()
{
    return consoleCharHeight;
}

DWORD CMacro::getConsoleDefaultAttributes()
{
    return consoleDefaultAttributes;
}

void CMacro::pushEvent(Event *pe)
{
    if( eventQueueLock.lock(5, __FUNCTION__) )
    {
        eventQueue.push(pe);
        eventQueueLock.unlock(__FUNCTION__);
    }
}

void CMacro::flushEvents()
{
    if( eventQueueLock.lock(INFINITE, __FUNCTION__) )
    {
        // Foreach and delete all
        while( eventQueue.size() )
        {
            Event *pe = eventQueue.front();
            delete pe;
            eventQueue.pop();
        }
        /*
        // Quickest and easiest way is to just make a new queue, then swap.
        std::queue<Event *> emptyQueue;
        swap(eventQueue, emptyQueue);*/
        eventQueueLock.unlock(__FUNCTION__);
    }
}

int CMacro::handleHidInput()
{
    hid.poll();

    // Handle keyboard/mouse
    for(unsigned int i = 0; i < KS_SIZE; i++)
    {
        if( hid.released(i) )
        {   // Key released
            Event *pe = new Event;
            if( i > VK_XBUTTON2 )
                pe->type = MicroMacro::EVENT_KEYRELEASED;
            else
                pe->type = MicroMacro::EVENT_MOUSERELEASED;

            MicroMacro::EventData ced;
            ced.setValue((int)i);
            pe->data.push_back(ced);

            ced.setValue(hid.getToggleState(i));
            pe->data.push_back(ced);

            try {
                pushEvent(pe);
            }
            catch( std::bad_alloc &ba ) {
                badAllocation();
            }
        }
        else if( hid.pressed(i) )
        {   // Key pressed
            Event *pe = new Event;
            if( i > VK_XBUTTON2 )
                pe->type = MicroMacro::EVENT_KEYPRESSED;
            else
                pe->type = MicroMacro::EVENT_MOUSEPRESSED;

            MicroMacro::EventData ced;
            ced.setValue((int)i);
            pe->data.push_back(ced);

            ced.setValue(hid.getToggleState(i));
            pe->data.push_back(ced);

            try {
                pushEvent(pe);
            }
            catch( std::bad_alloc &ba ) {
                badAllocation();
            }
        }
    }

    // Handle gamepads
    unsigned int polled = 0;
    for(unsigned int i = 0; i < GAMEPADS && polled < hid.getGamepadMaxIndex(); i++)
    {
        if( !hid.gamepadIsAvailable(i) )
            continue; // Gamepad is disconnected, skip it

        ++polled; // Once we've successfully polled the number of detected gamepads, break

        // Check all buttons on this gamepad
        for(unsigned int b = 0; b < GAMEPAD_BUTTONS; b++)
        {
            if( hid.joyPressed(i, b) )
            {
                Event *pe = new Event;
                pe->type = MicroMacro::EVENT_GAMEPADPRESSED;
                //pe->idata1 = i + 1;
                //pe->idata2 = b + 1;

                MicroMacro::EventData ced;
                ced.setValue((int)(i + 1));
                pe->data.push_back(ced);

                ced.setValue((int)(b + 1));
                pe->data.push_back(ced);

                try {
                    pushEvent(pe);
                }
                catch( std::bad_alloc &ba ) {
                    badAllocation();
                }
            }
            else if( hid.joyReleased(i, b) )
            {
                Event *pe = new Event;
                pe->type = MicroMacro::EVENT_GAMEPADRELEASED;
                //pe->idata1 = i + 1;
                //pe->idata2 = b + 1;

                MicroMacro::EventData ced;
                ced.setValue((int)(i + 1));
                pe->data.push_back(ced);

                ced.setValue((int)(b + 1));
                pe->data.push_back(ced);

                try {
                    pushEvent(pe);
                }
                catch( std::bad_alloc &ba ) {
                    badAllocation();
                }
            }
        }

        // Check POV (D-pad)
        if( hid.joyPOVChanged(i) )
        {
            Event *pe = new Event;
            pe->type = MicroMacro::EVENT_GAMEPADPOVCHANGED;
            MicroMacro::EventData ced;
            ced.setValue((int)(i + 1));
            pe->data.push_back(ced);

            ced.setValue((float)hid.joyPOV(i) / 100);
            pe->data.push_back(ced);

            try {
                pushEvent(pe);
            }
            catch( std::bad_alloc &ba ) {
                badAllocation();
            }
        }

        // Check axis
        for(unsigned int a = 1; a <= GAMEPAD_AXIS_COUNT; a++)
        {
            if( hid.joyAxisChanged(i, a) )
            {
                Event *pe = new Event;
                pe->type = MicroMacro::EVENT_GAMEPADAXISCHANGED;
                /*pe->idata1 = i + 1;
                pe->idata2 = a;
                pe->fdata3 = hid.joyAxis(i, a)/65535.0f*100;*/

                MicroMacro::EventData ced;
                ced.setValue((int)(i + 1));
                pe->data.push_back(ced);

                ced.setValue((int)a);
                pe->data.push_back(ced);

                ced.setValue((float)hid.joyAxis(i, a) / 65535.0f * 100);
                pe->data.push_back(ced);

                try {
                    pushEvent(pe);
                }
                catch( std::bad_alloc &ba ) {
                    badAllocation();
                }
            }
        }
    }

    return MicroMacro::ERR_OK;
}

int CMacro::handleEvents()
{
    int success = MicroMacro::ERR_OK;

    if( eventQueueLock.lock(INFINITE, __FUNCTION__) )
    {
        while( !eventQueue.empty() )
        {
            Event *pe = eventQueue.front();
            success = engine.runEvent(pe);
            delete pe;
            eventQueue.pop();

            if( success != MicroMacro::ERR_OK )
            {
                lua_pop(engine.getLuaState(), 1);
                break;
            }
        }
        eventQueueLock.unlock(__FUNCTION__);
    }

    #ifdef NETWORKING_ENABLED
    if( Socket_lua::socketListLock.lock(INFINITE, __FUNCTION__) )
    {
        SocketListIterator i;

        // Handle all queued events before considering deleting anything
        for(i = Socket_lua::socketList.begin(); i != Socket_lua::socketList.end(); ++i)
        {
            MicroMacro::Socket *pSocket = *i;
            if( pSocket->mutex.lock(INFINITE, __FUNCTION__) )
            {
                while( !pSocket->eventQueue.empty() )
                {
                    MicroMacro::Event *pe = &pSocket->eventQueue.front();
                    success = engine.runEvent(pe);
                    pSocket->eventQueue.pop();

                    if( success != MicroMacro::ERR_OK )
                    {
                        lua_pop(engine.getLuaState(), 1);
                        break;
                    }
                }

                pSocket->mutex.unlock(__FUNCTION__);
            }
        }


        // Now iterate over the list again and delete those marked for deletion
        i   =   Socket_lua::socketList.begin();
        while( i != Socket_lua::socketList.end() )
        {
            MicroMacro::Socket *pSocket = *i;
            if( pSocket->mutex.lock(INFINITE, __FUNCTION__) )
            {
                if( pSocket->deleteMe )
                {
                    i = Socket_lua::socketList.erase(i);
                    delete pSocket;
                    pSocket = NULL;
                }
                else
                {
                    pSocket->mutex.unlock(__FUNCTION__);
                    ++i;
                }
            }
        }

        // Finally we can unlock
        Socket_lua::socketListLock.unlock(__FUNCTION__);
    }
    #endif

    return success;
}
