/*==LICENSE==*

CyanWorlds.com Engine - MMOG client, server and tools
Copyright (C) 2011  Cyan Worlds, Inc.

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <http://www.gnu.org/licenses/>.

Additional permissions under GNU GPL version 3 section 7

If you modify this Program, or any covered work, by linking or
combining it with any of RAD Game Tools Bink SDK, Autodesk 3ds Max SDK,
NVIDIA PhysX SDK, Microsoft DirectX SDK, OpenSSL library, Independent
JPEG Group JPEG library, Microsoft Windows Media SDK, or Apple QuickTime SDK
(or a modified version of those libraries),
containing parts covered by the terms of the Bink SDK EULA, 3ds Max EULA,
PhysX SDK EULA, DirectX SDK EULA, OpenSSL and SSLeay licenses, IJG
JPEG Library README, Windows Media SDK EULA, or QuickTime SDK EULA, the
licensors of this Program grant you additional
permission to convey the resulting work. Corresponding Source for a
non-source form of such a combination shall include the source code for
the parts of OpenSSL and IJG JPEG Library used as well as that of the covered
work.

You can contact Cyan Worlds, Inc. by email legal@cyan.com
 or by snail mail at:
      Cyan Worlds, Inc.
      14617 N Newport Hwy
      Mead, WA   99021

*==LICENSE==*/
//////////////////////////////////////////////////////////////////////////////
//                                                                          //
//  pfConsole Functions                                                     //
//                                                                          //
//////////////////////////////////////////////////////////////////////////////

#include "pfConsole.h"
#include "pfConsoleCore/pfConsoleEngine.h"

#include "HeadSpin.h"
#include "plFileSystem.h"
#include "plgDispatch.h"
#include "plPipeline.h"
#include "plProduct.h"
#include "hsTimer.h"

#include "plGImage/plPNG.h"
#include "plInputCore/plInputDevice.h"
#include "plInputCore/plInputInterface.h"
#include "plInputCore/plInputInterfaceMgr.h"
#include "pnInputCore/plKeyDef.h"
#include "pnInputCore/plKeyMap.h"
#include "pnKeyedObject/plFixedKey.h"
#include "plMessage/plInputEventMsg.h"
#include "plMessage/plCaptureRenderMsg.h"
#include "plMessage/plConsoleMsg.h"
#include "plMessage/plInputIfaceMgrMsg.h"
#include "plNetClient/plNetClientMgr.h"
#include "plPipeline/plDebugText.h"
#include "pfPython/cyPythonInterface.h"

#ifndef PLASMA_EXTERNAL_RELEASE
#include "pfGameMgr/pfGameMgr.h"
#endif // PLASMA_EXTERNAL_RELEASE


//// Static Class Stuff //////////////////////////////////////////////////////

pfConsole   *pfConsole::fTheConsole = nil;
uint32_t      pfConsole::fConsoleTextColor = 0xff00ff00;
plPipeline  *pfConsole::fPipeline = nil;


//////////////////////////////////////////////////////////////////////////////
//// pfConsoleInputInterface - Input interface layer for the console /////////
//////////////////////////////////////////////////////////////////////////////

class pfConsoleInputInterface : public plInputInterface
{
    protected:

        pfConsole   *fConsole;



        virtual bool    IHandleCtrlCmd( plCtrlCmd *cmd )
        {
            if( cmd->fControlCode == B_SET_CONSOLE_MODE )
            {
                if( cmd->fControlActivated )
                {
                    // Activate/deactivate
                    switch( fConsole->fMode )
                    {
                        case pfConsole::kModeHidden:
                            fConsole->ISetMode( pfConsole::kModeSingleLine );
                            break;
                        case pfConsole::kModeSingleLine:
                            fConsole->ISetMode( pfConsole::kModeFull );
                            break;
                        case pfConsole::kModeFull:
                            fConsole->ISetMode( pfConsole::kModeHidden );
                            break;
                    }
                }
                return true;
            }
            return false;
        }

    public:

        pfConsoleInputInterface( pfConsole *console )
        {
            fConsole = console; 
            SetEnabled( true );         // Always enabled

            // Add our control codes to our control map. Do NOT add the key bindings yet.
            // Note: HERE is where you specify the actions for each command, i.e. net propagate and so forth.
            // This part basically declares us master of the bindings for these commands.
            
            // IF YOU ARE LOOKING TO CHANGE THE DEFAULT KEY BINDINGS, DO NOT LOOK HERE. GO TO
            // RestoreDefaultKeyMappings()!!!!

#ifndef PLASMA_EXTERNAL_RELEASE
            fControlMap->AddCode( B_SET_CONSOLE_MODE, kControlFlagNormal | kControlFlagNoRepeat );
#endif

            // IF YOU ARE LOOKING TO CHANGE THE DEFAULT KEY BINDINGS, DO NOT LOOK HERE. GO TO
            // RestoreDefaultKeyMappings()!!!!
        }

        virtual uint32_t  GetPriorityLevel( void ) const          { return kConsolePriority; }
        virtual uint32_t  GetCurrentCursorID( void ) const        { return kCursorHidden; }
        virtual bool    HasInterestingCursorID( void ) const    { return false; }

        virtual bool    InterpretInputEvent( plInputEventMsg *pMsg )
        {
            plKeyEventMsg   *keyMsg = plKeyEventMsg::ConvertNoRef( pMsg );
            if( keyMsg != nil )
            {
                if( fConsole->fMode )
                {
                    fConsole->IHandleKey( keyMsg );
                    return true;
                }
            }

            return false;
        }

        virtual void    RefreshKeyMap( void )
        {
        }

        virtual void    RestoreDefaultKeyMappings( void )
        {
            if( fControlMap != nil )
            {
                fControlMap->UnmapAllBindings();
#ifndef PLASMA_EXTERNAL_RELEASE
                fControlMap->BindKey( KEY_TILDE, B_SET_CONSOLE_MODE );
#endif
            }
        }
};

//// Constructor & Destructor ////////////////////////////////////////////////

pfConsole::pfConsole()
{
    fNumDisplayLines = 32;
    fDisplayBuffer = nil;
    fTheConsole = this;
    fFXEnabled = true;
}

pfConsole::~pfConsole()
{
    if( fInputInterface != nil )
    {
        plInputIfaceMgrMsg *msg = new plInputIfaceMgrMsg( plInputIfaceMgrMsg::kRemoveInterface );
        msg->SetIFace( fInputInterface );
        plgDispatch::MsgSend( msg );

        hsRefCnt_SafeUnRef( fInputInterface );
        fInputInterface = nil;
    }

    if( fDisplayBuffer != nil )
    {
        delete [] fDisplayBuffer;
        fDisplayBuffer = nil;
    }

    fTheConsole = nil;

    plgDispatch::Dispatch()->UnRegisterForExactType( plConsoleMsg::Index(), GetKey() );
    plgDispatch::Dispatch()->UnRegisterForExactType( plControlEventMsg::Index(), GetKey() );

#ifndef PLASMA_EXTERNAL_RELEASE 
    pfGameMgr::GetInstance()->RemoveReceiver(GetKey());
#endif // PLASMA_EXTERNAL_RELEASE
}

pfConsole * pfConsole::GetInstance () {

    return fTheConsole;
}

//// Init ////////////////////////////////////////////////////////////////////

void    pfConsole::Init( pfConsoleEngine *engine )
{
    fDisplayBuffer = new char[ fNumDisplayLines * kMaxCharsWide ];
    memset( fDisplayBuffer, 0, fNumDisplayLines * kMaxCharsWide );

    memset( fWorkingLine, 0, sizeof( fWorkingLine ) );
    fWorkingCursor = 0;

    memset( fHistory, 0, sizeof( fHistory ) );

    fEffectCounter = 0;
    fMode = 0;
    fMsgTimeoutTimer = 0;
    fHelpMode = false;
    fPythonMode = false;
    fPythonFirstTime = true;
    fPythonMultiLines = 0;
    fHelpTimer = 0;
    fCursorTicks = 0;
    memset( fLastHelpMsg, 0, sizeof( fLastHelpMsg ) );
    fEngine = engine;

    fInputInterface = new pfConsoleInputInterface( this );
    plInputIfaceMgrMsg *msg = new plInputIfaceMgrMsg( plInputIfaceMgrMsg::kAddInterface );
    msg->SetIFace( fInputInterface );
    plgDispatch::MsgSend( msg );

    // Register for keyboard event messages
    plgDispatch::Dispatch()->RegisterForExactType( plConsoleMsg::Index(), GetKey() );
    plgDispatch::Dispatch()->RegisterForExactType( plControlEventMsg::Index(), GetKey() );

#ifndef PLASMA_EXTERNAL_RELEASE 
    pfGameMgr::GetInstance()->AddReceiver(GetKey());
#endif // PLASMA_EXTERNAL_RELEASE
}

//// ISetMode ////////////////////////////////////////////////////////////////

void    pfConsole::ISetMode( uint8_t mode )
{
    fMode = mode; 
    fEffectCounter = ( fFXEnabled ? kEffectDivisions : 0 ); 
    fMsgTimeoutTimer = 0; 
    fInputInterface->RefreshKeyMap();
}

//// MsgReceive //////////////////////////////////////////////////////////////

#include <algorithm>
bool    pfConsole::MsgReceive( plMessage *msg )
{
    // Handle screenshot saving...
    plCaptureRenderMsg* capMsg = plCaptureRenderMsg::ConvertNoRef(msg);
    if (capMsg) {
        plFileName screenshots = plFileName::Join(plFileSystem::GetUserDataPath(), "Screenshots");
        plFileSystem::CreateDir(screenshots, false); // just in case...
        plString prefix = plProduct::ShortName();

        // List all of the PNG indices we have taken up already...
        plString pattern = plFormat("{}*.png", prefix);
        std::vector<plFileName> images = plFileSystem::ListDir(screenshots, pattern.c_str());
        std::set<uint32_t> indices;
        std::for_each(images.begin(), images.end(),
            [&] (const plFileName& fn) {
                plString idx = fn.GetFileNameNoExt().Substr(prefix.GetSize());
                indices.insert(idx.ToUInt(10));
            }
        );

        // Now that we have an ordered set of indices, save this screenshot to the first one we don't have.
        uint32_t num = 0;
        for (auto it = indices.begin(); it != indices.end(); ++it, ++num) {
            if (*it != num)
                break;
        }

        // Got our num, save the screenshot.
        plFileName fn = plFormat("{}{_04}.png", prefix, num);
        plPNG::Instance().WriteToFile(plFileName::Join(screenshots, fn), capMsg->GetMipmap());

        AddLineF("Saved screenshot as '%s'", fn.AsString().c_str());
        return true;
    }

    plControlEventMsg *ctrlMsg = plControlEventMsg::ConvertNoRef( msg );
    if( ctrlMsg != nil )
    {
        if( ctrlMsg->ControlActivated() && ctrlMsg->GetControlCode() == B_CONTROL_CONSOLE_COMMAND && plNetClientMgr::GetInstance()->GetFlagsBit(plNetClientMgr::kPlayingGame))
        {
            fEngine->RunCommand( ctrlMsg->GetCmdString(), IAddLineCallback );
            return true;
        }
        return false;
    }

    plConsoleMsg *cmd = plConsoleMsg::ConvertNoRef( msg );
    if( cmd != nil && cmd->GetString() != nil )
    {
        if( cmd->GetCmd() == plConsoleMsg::kExecuteFile )
        {
            if( !fEngine->ExecuteFile( cmd->GetString() ) )
            {
                // Change the following line once we have a better way of reporting
                // errors in the parsing
                plString str = plFormat("Error parsing {}", cmd->GetString());
                plString msg = plFormat("{}:\n\nCommand: '{}'\n{}", fEngine->GetErrorMsg(), fEngine->GetLastErrorLine(),
#ifdef HS_DEBUGGING
                        "" );

                hsAssert( false, msg.c_str() );
#else
                        "\nPress OK to continue parsing files." );

                hsMessageBox( msg.c_str(), str.c_str(), hsMessageBoxNormal );
#endif
            }
        }
        else if( cmd->GetCmd() == plConsoleMsg::kAddLine )
            IAddParagraph( cmd->GetString() );
        else if( cmd->GetCmd() == plConsoleMsg::kExecuteLine )
        {
            if( !fEngine->RunCommand( (char *)cmd->GetString(), IAddLineCallback ) )
            {
                // Change the following line once we have a better way of reporting
                // errors in the parsing
                AddLineF("%s:\n\nCommand: '%s'\n", fEngine->GetErrorMsg(), fEngine->GetLastErrorLine());
            }
        }
            
        return true;
    }


    //========================================================================
    // pfGameMgrMsg
#ifndef PLASMA_EXTERNAL_RELEASE
    if (pfGameMgrMsg * gameMgrMsg = pfGameMgrMsg::ConvertNoRef(msg)) {

        switch (gameMgrMsg->netMsg->messageId) {

            //================================================================
            // InviteReceived           
            case kSrv2Cli_GameMgr_InviteReceived: {
                const Srv2Cli_GameMgr_InviteReceived & gmMsg = *(const Srv2Cli_GameMgr_InviteReceived *)gameMgrMsg->netMsg;
                const plString & inviterName = plNetClientMgr::GetInstance()->GetPlayerNameById(gmMsg.inviterId);
                AddLineF("[GameMgr] Invite received: %S, %u. Inviter: %s", pfGameMgr::GetInstance()->GetGameNameByTypeId(gmMsg.gameTypeId), gmMsg.newGameId, inviterName.c_str("<Unknown>"));
            }
            return true;

            //================================================================
            // InviteRevoked            
            case kSrv2Cli_GameMgr_InviteRevoked: {
                const Srv2Cli_GameMgr_InviteRevoked & gmMsg = *(const Srv2Cli_GameMgr_InviteRevoked *)gameMgrMsg->netMsg;
                const plString & inviterName = plNetClientMgr::GetInstance()->GetPlayerNameById(gmMsg.inviterId);
                AddLineF("[GameMgr] Invite revoked: %S, %u. Inviter: %s", pfGameMgr::GetInstance()->GetGameNameByTypeId(gmMsg.gameTypeId), gmMsg.newGameId, inviterName.c_str("<Unknown>"));
            }
            return true;
            
            DEFAULT_FATAL(gameMgrMsg->netMsg->messageId);
        };
    }
#endif // PLASMA_EXTERNAL_RELEASE
    //========================================================================
    
    //========================================================================
    // pfGameCliMsg
#ifndef PLASMA_EXTERNAL_RELEASE
    if (pfGameCliMsg * gameCliMsg = pfGameCliMsg::ConvertNoRef(msg)) {
    
        pfGameCli * cli = gameCliMsg->gameCli;
    
        //====================================================================
        // Handle pfGameCli msgs
        switch (gameCliMsg->netMsg->messageId) {
            //================================================================
            // PlayerJoined
            case kSrv2Cli_Game_PlayerJoined: {
                const Srv2Cli_Game_PlayerJoined & netMsg = *(const Srv2Cli_Game_PlayerJoined *)gameCliMsg->netMsg;
                AddLineF(
                    "[Game %s:%u] Player joined: %s",
                    cli->GetName(),
                    cli->GetGameId(),
                    netMsg.playerId
                        ? plNetClientMgr::GetInstance()->GetPlayerNameById(netMsg.playerId).c_str()
                        : "Computer"
                );
            }
            return true;

            //================================================================
            // PlayerLeft
            case kSrv2Cli_Game_PlayerLeft: {
                const Srv2Cli_Game_PlayerLeft & netMsg = *(const Srv2Cli_Game_PlayerLeft *)gameCliMsg->netMsg;
                AddLineF(
                    "[Game %s:%u] Player left: %s",
                    cli->GetName(),
                    cli->GetGameId(),
                    netMsg.playerId
                        ? plNetClientMgr::GetInstance()->GetPlayerNameById(netMsg.playerId).c_str()
                        : "Computer"
                );
            }
            return true;
            
            //================================================================
            // InviteFailed
            case kSrv2Cli_Game_InviteFailed: {
                const Srv2Cli_Game_InviteFailed & netMsg = *(const Srv2Cli_Game_InviteFailed *)gameCliMsg->netMsg;
                AddLineF(
                    "[Game %s:%u] Invite failed for playerId %u, error %u",
                    cli->GetName(),
                    cli->GetGameId(),
                    netMsg.inviteeId,
                    netMsg.error
                );
            }
            return true;

            //================================================================
            // OwnerChange
            case kSrv2Cli_Game_OwnerChange: {
                const Srv2Cli_Game_OwnerChange & netMsg = *(const Srv2Cli_Game_OwnerChange *)gameCliMsg->netMsg;
                AddLineF(
                    "[Game %s:%u] Owner changed to playerId %u",
                    cli->GetName(),
                    cli->GetGameId(),
                    netMsg.ownerId
                );
            }
            return true;
        }

        //====================================================================
        // Handle Tic-Tac-Toe msgs
        if (gameCliMsg->gameCli->GetGameTypeId() == kGameTypeId_TicTacToe) {
        
            pfGmTicTacToe * ttt = pfGmTicTacToe::ConvertNoRef(cli);
            ASSERT(ttt);
            
            switch (gameCliMsg->netMsg->messageId) {
                //============================================================
                // GameStarted
                case kSrv2Cli_TTT_GameStarted: {
                    const Srv2Cli_TTT_GameStarted & netMsg = *(const Srv2Cli_TTT_GameStarted *)gameCliMsg->netMsg;
                    if (netMsg.yourTurn)
                        AddLineF(
                            "[Game %s:%u] Game started. You are X's. You go first.",
                            cli->GetName(),
                            cli->GetGameId()
                        );
                    else
                        AddLineF(
                            "[Game %s:%u] Game started. You are O's. Other player goes first.",
                            cli->GetName(),
                            cli->GetGameId()
                        );
                    ttt->ShowBoard();
                }
                return true;

                //============================================================
                // GameOver
                case kSrv2Cli_TTT_GameOver: {
                    const Srv2Cli_TTT_GameOver & netMsg = *(const Srv2Cli_TTT_GameOver *)gameCliMsg->netMsg;
                    switch (netMsg.result) {
                        case kTTTGameResultWinner:
                            if (netMsg.winnerId == NetCommGetPlayer()->playerInt)
                                AddLineF(
                                    "[Game %s:%u] Game over. You won!",
                                    cli->GetName(),
                                    cli->GetGameId()
                                );
                            else
                                AddLineF(
                                    "[Game %s:%u] Game over. You lost.",
                                    cli->GetName(),
                                    cli->GetGameId()
                                );
                        break;
                        
                        case kTTTGameResultTied:
                            AddLineF(
                                "[Game %s:%u] Game over. You tied.",
                                cli->GetName(),
                                cli->GetGameId()
                            );
                        break;
                        
                        case kTTTGameResultGave:
                            AddLineF(
                                "[Game %s:%u] Game over. You win by default.",
                                cli->GetName(),
                                cli->GetGameId()
                            );
                        break;
                            
                        default:
                            AddLineF(
                                "[Game %s:%u] Game over. Server-side error %u.",
                                cli->GetName(),
                                cli->GetGameId(),
                                netMsg.result
                            );
                        break;
                    }
                }
                return true;
                
                //============================================================
                // MoveMade
                case kSrv2Cli_TTT_MoveMade: {
                    const Srv2Cli_TTT_MoveMade & netMsg = *(const Srv2Cli_TTT_MoveMade *)gameCliMsg->netMsg;
                    const char * playerName
                        = netMsg.playerId
                            ? plNetClientMgr::GetInstance()->GetPlayerNameById(netMsg.playerId).c_str()
                            : "Computer";
                    AddLineF(
                        "[Game %s:%u] %s moved:",
                        cli->GetName(),
                        cli->GetGameId(),
                        playerName
                    );
                    ttt->ShowBoard();
                }
                return true;
                
                DEFAULT_FATAL(gameCliMsg->netMsg->messageId);
            }
        }
        else {
            FATAL("Unknown game type");
        }
        
        return true;
    }
#endif // PLASMA_EXTERNAL_RELEASE
    //========================================================================
    
    return hsKeyedObject::MsgReceive(msg);
}

//// IHandleKey //////////////////////////////////////////////////////////////

void    pfConsole::IHandleKey( plKeyEventMsg *msg )
{
    char            *c;
    wchar_t         key;
    int             i,eol;
    static bool     findAgain = false;
    static uint32_t   findCounter = 0;

    // filter out keyUps and ascii control characters
    // as the control functions are handled on the keyDown event
    if( !msg->GetKeyDown() || (msg->GetKeyChar() > '\0' && msg->GetKeyChar() < ' '))
        return;

    if( msg->GetKeyCode() == KEY_ESCAPE )
    {
        fWorkingLine[ fWorkingCursor = 0 ] = 0;
        findAgain = false;
        findCounter = 0;
        fHelpMode = false;
        fPythonMode = false;
        fPythonMultiLines = 0;
        IUpdateTooltip();
    }
    else if( msg->GetKeyCode() == KEY_TAB )
    {
        if ( fPythonMode )
        {
            // if we are in Python mode, then just add two spaces, tab over, sorta
            if ( strlen(fWorkingLine) < kWorkingLineSize+2 )
            {
                strcat(&fWorkingLine[fWorkingCursor], "  ");
                fWorkingCursor += 2;
            }
        }
        else
        {
            static char     lastSearch[ kMaxCharsWide ];
            char            search[ kMaxCharsWide ];

            if( !findAgain && findCounter == 0 )
                strcpy( lastSearch, fWorkingLine );
            strcpy( search, lastSearch );

            if( findCounter > 0 )
            {
                // Not found the normal way; try using an unrestricted search
                if( fEngine->FindNestedPartialCmd( search, findCounter, true ) )
                {
                    strcpy( fWorkingLine, search );
                    findCounter++;
                }
                else
                {
                    /// Try starting over...?
                    findCounter = 0;
                    if( fEngine->FindNestedPartialCmd( search, findCounter, true ) )
                    { 
                        strcpy( fWorkingLine, search );
                        findCounter++;
                    }
                }
            }
            else if( fEngine->FindPartialCmd( search, findAgain, true ) )
            {
                strcpy( fWorkingLine, search );
                findAgain = true;
            }
            else if( findAgain )
            {
                /// Try starting over
                strcpy( search, lastSearch );
                if( fEngine->FindPartialCmd( search, findAgain = false, true ) )
                {
                    strcpy( fWorkingLine, search );
                    findAgain = true;
                }
            }

            else
            {
                // Not found the normal way; start an unrestricted search
                if( fEngine->FindNestedPartialCmd( search, findCounter, true ) )
                {
                    strcpy( fWorkingLine, search );
                    findCounter++;
                }
            }

            fWorkingCursor = strlen( fWorkingLine );
            IUpdateTooltip();
        }
    }
    else if( msg->GetKeyCode() == KEY_UP )
    {
        i = ( fHistory[ fPythonMode ].fRecallCursor > 0 ) ? fHistory[ fPythonMode ].fRecallCursor - 1 : kNumHistoryItems - 1;
        if( fHistory[ fPythonMode ].fData[ i ][ 0 ] != 0 )
        {
            fHistory[ fPythonMode ].fRecallCursor = i;
            strcpy( fWorkingLine, fHistory[ fPythonMode ].fData[ fHistory[ fPythonMode ].fRecallCursor ] );
            findAgain = false;
            findCounter = 0;
            fWorkingCursor = strlen( fWorkingLine );
            IUpdateTooltip();
        }
    }
    else if( msg->GetKeyCode() == KEY_DOWN )
    {
        if( fHistory[ fPythonMode ].fRecallCursor != fHistory[ fPythonMode ].fCursor )
        {
            i = ( fHistory[ fPythonMode ].fRecallCursor < kNumHistoryItems - 1 ) ? fHistory[ fPythonMode ].fRecallCursor + 1 : 0;
            if( i != fHistory[ fPythonMode ].fCursor )
            {
                fHistory[ fPythonMode ].fRecallCursor = i;
                strcpy( fWorkingLine, fHistory[ fPythonMode ].fData[ fHistory[ fPythonMode ].fRecallCursor ] );
            }
            else
            {
                memset( fWorkingLine, 0, sizeof( fWorkingLine ) );
                fHistory[ fPythonMode ].fRecallCursor = fHistory[ fPythonMode ].fCursor;
            }
            findAgain = false;
            findCounter = 0;
            fWorkingCursor = strlen( fWorkingLine );
            IUpdateTooltip();
        }
    }
    else if( msg->GetKeyCode() == KEY_LEFT )
    {
        if( fWorkingCursor > 0 )
            fWorkingCursor--;
    }
    else if( msg->GetKeyCode() == KEY_RIGHT )
    {
        if( fWorkingCursor < strlen( fWorkingLine ) )
            fWorkingCursor++;
    }
    else if( msg->GetKeyCode() == KEY_BACKSPACE )
    {
        if( fWorkingCursor > 0 )
        {
            fWorkingCursor--;

            c = &fWorkingLine[ fWorkingCursor ];
            do
            {
                *c = *( c + 1 );
                c++;
            } while( *c != 0 );

            findAgain = false;
            findCounter = 0;
        }
        else if( fHelpMode )
            fHelpMode = false;

        IUpdateTooltip();
    }
    else if( msg->GetKeyCode() == KEY_DELETE )
    {
        c = &fWorkingLine[ fWorkingCursor ];
        do
        {
            *c = *( c + 1 );
            c++;
        } while( *c != 0 );

        findAgain = false;
        findCounter = 0;
        IUpdateTooltip();
    }
    else if( msg->GetKeyCode() == KEY_ENTER )
    {
        // leave leading space for Python multi lines (need the indents!)
        if ( fPythonMultiLines == 0 )
        {
            // Clean up working line by removing any leading whitespace
            for( c = fWorkingLine; *c == ' ' || *c == '\t'; c++ );
            for( i = 0; *c != 0; i++, c++ )
                fWorkingLine[ i ] = *c;
            fWorkingLine[ i ] = 0;
            eol = i;
        }

        if( fWorkingLine[ 0 ] == 0 && !fHelpMode && !fPythonMode )
        {
            // Blank line--just print a blank line to the console and skip
            IAddLine( "" );
            return;
        }

        // only save history line if there is something there
        if( fWorkingLine[ 0 ] != 0 )
        {
            // Save to history
            strcpy( fHistory[ fPythonMode ].fData[ fHistory[ fPythonMode ].fCursor ], fWorkingLine );
            fHistory[ fPythonMode ].fCursor = ( fHistory[ fPythonMode ].fCursor < kNumHistoryItems - 1 ) ? fHistory[ fPythonMode ].fCursor + 1 : 0;
            fHistory[ fPythonMode ].fRecallCursor = fHistory[ fPythonMode ].fCursor;
        }

        // EXECUTE!!! (warning: DESTROYS fWorkingLine)
        if( fHelpMode )
        {
            if( fWorkingLine[ 0 ] == 0 )
                IPrintSomeHelp();
            else if( stricmp( fWorkingLine, "commands" ) == 0 )
                fEngine->PrintCmdHelp( "", IAddLineCallback );
            else if( !fEngine->PrintCmdHelp( fWorkingLine, IAddLineCallback ) )
            {
                c = (char *)fEngine->GetErrorMsg();
                AddLine( c );
            }

            fHelpMode = false;
        }
        // are we in Python mode?
        else if ( fPythonMode )
        {
            // are we in Python multi-line mode?
            if ( fPythonMultiLines > 0 )
            {
                // if there was a line then bump num lines
                if ( fWorkingLine[0] != 0 )
                {
                    AddLineF("... %s", fWorkingLine);
                    fPythonMultiLines++;
                }

                // is it time to evaluate all the multi lines that are saved?
                if ( fWorkingLine[0] == 0 || fPythonMultiLines >= kNumHistoryItems )
                {
                    if ( fPythonMultiLines >= kNumHistoryItems )
                        AddLine("Python Multi-line buffer full!");
                    // get the lines and stuff them in our buffer
                    char biglines[kNumHistoryItems*(kMaxCharsWide+1)];
                    biglines[0] = 0;
                    for ( i=fPythonMultiLines; i>0 ; i--)
                    {
                        // reach back in the history and find this line and paste it in here
                        int recall = fHistory[ fPythonMode ].fCursor - i;
                        if ( recall < 0 )
                            recall += kNumHistoryItems;
                        strcat(biglines,fHistory[ fPythonMode ].fData[ recall ]);
                        strcat(biglines,"\n");
                    }
                    // now evaluate this mess they made
                    PyObject* mymod = PythonInterface::FindModule("__main__");
                    PythonInterface::RunStringInteractive(biglines,mymod);
                    std::string output;
                    // get the messages
                    PythonInterface::getOutputAndReset(&output);
                    AddLine( output.c_str() );
                    // all done doing multi lines...
                    fPythonMultiLines = 0;
                }
            }
            // else we are just doing single lines
            else
            {
                // was there actually anything in the input buffer?
                if ( fWorkingLine[0] != 0 )
                {
                    AddLineF(">>> %s", fWorkingLine);
                    // check to see if this is going to be a multi line mode ( a ':' at the end)
                    if ( fWorkingLine[eol-1] == ':' )
                    {
                        fPythonMultiLines = 1;
                    }
                    else
                    // else if not the start of a multi-line then execute it
                    {
                        PyObject* mymod = PythonInterface::FindModule("__main__");
                        PythonInterface::RunStringInteractive(fWorkingLine,mymod);
                        std::string output;
                        // get the messages
                        PythonInterface::getOutputAndReset(&output);
                        AddLine( output.c_str() );
                    }
                }
                else
                    AddLine( ">>> " );
            }
            // find the end of the line
            for( c = fWorkingLine, eol = 0; *c != 0; eol++, c++ );
        }
        else
        {
            if( !fEngine->RunCommand( fWorkingLine, IAddLineCallback ) )
            {
                c = (char *)fEngine->GetErrorMsg();
                if( c[ 0 ] != 0 )
                    AddLine( c );
            }
        }

        // Clear
        fWorkingLine[ fWorkingCursor = 0 ] = 0;
        findAgain = false;
        findCounter = 0;
        IUpdateTooltip();
    }
    else if( msg->GetKeyCode() == KEY_END )
    {
        fWorkingCursor = strlen( fWorkingLine );
    }
    else if( msg->GetKeyCode() == KEY_HOME )
    {
        fWorkingCursor = 0;
    }
    else if (msg->GetKeyChar())
    {
        key = msg->GetKeyChar();
        // do they want to go into help mode?
        if( !fPythonMode && key == L'?' && fWorkingCursor == 0 )
        {
            /// Go into help mode
            fHelpMode = true;
        }
        // do they want to go into Python mode?
        else if( !fHelpMode && key == L'\\' && fWorkingCursor == 0 )
        {
            // toggle Python mode
            fPythonMode = fPythonMode ? false:true;
            if ( fPythonMode )
            {
                if ( fPythonFirstTime )
                {
                    IAddLine( "" );     // add a blank line
                    PyObject* mymod = PythonInterface::FindModule("__main__");
                    PythonInterface::RunStringInteractive("import sys;print 'Python',sys.version",mymod);
                    std::string output;
                    // get the messages
                    PythonInterface::getOutputAndReset(&output);
                    AddLine( output.c_str() );
                    fPythonFirstTime = false;       // do this only once!
                }
            }
        }
        // or are they just typing in a working line
        else if( strlen( fWorkingLine ) < kMaxCharsWide - 2 && key != 0 )
        {
            for( i = strlen( fWorkingLine ) + 1; i > fWorkingCursor; i-- )
                fWorkingLine[ i ] = fWorkingLine[ i - 1 ];

            fWorkingLine[ fWorkingCursor++ ] = key;

            findAgain = false;
            findCounter = 0;
            IUpdateTooltip();
        }
    }
}

//// IAddLineCallback ////////////////////////////////////////////////////////

void    pfConsole::IAddLineCallback( const char *string )
{
    fTheConsole->IAddParagraph( string, 0 );
}

//// IAddLine ////////////////////////////////////////////////////////////////

void    pfConsole::IAddLine( const char *string, short leftMargin )
{
    int         i;
    char        *ptr;


    /// Advance upward
    for( i = 0, ptr = fDisplayBuffer; i < fNumDisplayLines - 1; i++ )
    {
        memcpy( ptr, ptr + kMaxCharsWide, kMaxCharsWide );
        ptr += kMaxCharsWide;
    }

    if( string[ 0 ] == '\t' )
    {
        leftMargin += 4;
        string++;
    }

    memset( ptr, 0, kMaxCharsWide );
    memset( ptr, ' ', leftMargin );
    strncpy( ptr + leftMargin, string, kMaxCharsWide - leftMargin - 1 );
    if( fMode == 0 )
    {
        /// Console is invisible, so show this line for a bit of time
        fMsgTimeoutTimer = kMsgHintTimeout;
    }
}

//// IAddParagraph ///////////////////////////////////////////////////////////

void    pfConsole::IAddParagraph( const char *s, short margin )
{
    char        *ptr, *ptr2, *ptr3, *string=(char*)s;


    // Special character: if \i is in front of the string, indent it
    while( strncmp( string, "\\i", 2 ) == 0 )
    {
        margin += 3;
        string += 2;
    }

    for( ptr = string; ptr != nil && *ptr != 0; )
    {
        // Go as far as possible
        if( strlen( ptr ) < kMaxCharsWide - margin - margin - 1 )
            ptr2 = ptr + strlen( ptr );
        else
        {
            // Back up until we hit a sep
            ptr2 = ptr + kMaxCharsWide - margin - margin - 1;
            for( ; ptr2 > string && *ptr2 != ' ' && *ptr2 != '\t' && *ptr2 != '\n'; ptr2-- );
        }

        // Check for carriage return
        ptr3 = strchr( ptr, '\n' );
        if( ptr3 == ptr )
        {
            IAddLine( "", margin );
            ptr++;
            continue;
        }
        if( ptr3 != nil && ptr3 < ptr2 )
            ptr2 = ptr3;

        // Add this part
        if( ptr2 == ptr || *ptr2 == 0 )
        {
            IAddLine( ptr, margin );
            break;
        }

        *ptr2 = 0;
        IAddLine( ptr, margin );
        ptr = ptr2 + 1;
    }
}

//// IClear //////////////////////////////////////////////////////////////////

void    pfConsole::IClear( void )
{
    memset( fDisplayBuffer, 0, kMaxCharsWide * fNumDisplayLines );
}

//// Draw ////////////////////////////////////////////////////////////////////

void    pfConsole::Draw( plPipeline *p )
{
    int         i, yOff, y, x, eOffset, height;
    char        *line;
    char        tmp[ kMaxCharsWide ];
    bool        showTooltip = false;
    float       thisTime;   // For making the console FX speed konstant regardless of framerate
    const float kEffectDuration = 0.5f;


    plDebugText&    drawText = plDebugText::Instance();

    thisTime = hsTimer::GetSeconds<float>();

    if( fMode == kModeHidden && fEffectCounter == 0 )
    {
        if( fMsgTimeoutTimer > 0 )
        {
            /// Message hint--draw the last line of the console for a bit
            drawText.DrawString( 10, 4, fDisplayBuffer + kMaxCharsWide * ( fNumDisplayLines - 1 ), fConsoleTextColor );
            fMsgTimeoutTimer--;
        }
        fLastTime = thisTime;
        return;
    }

    drawText.SetDrawOnTopMode( true );

    yOff = drawText.GetFontHeight() + 2;
    if( fMode == kModeSingleLine )
        height = yOff * 3 + 14;
    else
        height = yOff * ( fNumDisplayLines + 2 ) + 14;

    if( fHelpTimer == 0 && !fHelpMode && fLastHelpMsg[ 0 ] != 0 )
        showTooltip = true;
    
    if( fEffectCounter > 0 )
    {
        int numElapsed = (int)( (float)kEffectDivisions * ( ( thisTime - fLastTime ) / (float)kEffectDuration ) );
        if( numElapsed > fEffectCounter )
            numElapsed = fEffectCounter;
        else if( numElapsed < 0 )
            numElapsed = 0;

        if( fMode == kModeSingleLine )
            eOffset = fEffectCounter * height / kEffectDivisions;
        else if( fMode == kModeFull )
            eOffset = fEffectCounter * ( height - yOff * 3 - 14 ) / kEffectDivisions;
        else
            eOffset = ( kEffectDivisions - fEffectCounter ) * ( height - yOff * 3 - 14 ) / kEffectDivisions;
        fEffectCounter -= numElapsed;
    }
    else 
        eOffset = 0;
    fLastTime = thisTime;

    if( fMode == kModeSingleLine )
    {
        // Bgnd (TEMP ONLY)
        x = kMaxCharsWide * drawText.CalcStringWidth( "W" ) + 4;
        y = height - eOffset;
        drawText.DrawRect( 4, 0, x, y, /*color*/0, 0, 0, 127 );

        /// Actual text
        if( fEffectCounter == 0 )
            drawText.DrawString( 10, 4, "Plasma 2.0 Console", 255, 255, 255, 255 );

        if( !showTooltip )
            drawText.DrawString( 10, 4 + yOff - eOffset, fDisplayBuffer + kMaxCharsWide * ( fNumDisplayLines - 1 ), fConsoleTextColor );

        y = 4 + yOff + yOff - eOffset;
    }
    else
    {
        // Bgnd (TEMP ONLY)
        x = kMaxCharsWide * drawText.CalcStringWidth( "W" ) + 4;
        y = yOff * ( fNumDisplayLines + 2 ) + 14 - eOffset;
        drawText.DrawRect( 4, 0, x, y, /*color*/0, 0, 0, 127 );

        /// Actual text
        drawText.DrawString( 10, 4, "Plasma 2.0 Console", 255, 255, 255, 255 );

        static int  countDown = 3000;
        if( fHelpTimer > 0 || fEffectCounter > 0 || fMode != kModeFull )
            countDown = 3000;
        else if( countDown > -720 )
            countDown--;

        // Resource data is encrypted so testers can't peer in to the EXE, plz don't decrypt
        static bool rezLoaded = false;
        static char tmpSrc[ kMaxCharsWide ];
        if( !rezLoaded )
        {
            memset( tmp, 0, sizeof( tmp ) );
            memset( tmpSrc, 0, sizeof( tmpSrc ) );
            // Our concession to windows
#ifdef HS_BUILD_FOR_WIN32
            #include "../../Apps/plClient/res/resource.h"
            HRSRC rsrc = FindResource( nil, MAKEINTRESOURCE( IDR_CNSL1 ), "CNSL" );
            if( rsrc != nil )
            {
                HGLOBAL hdl = LoadResource( nil, rsrc );
                if( hdl != nil )
                {
                    uint8_t *ptr = (uint8_t *)LockResource( hdl );
                    if( ptr != nil )
                    {
                        for( i = 0; i < SizeofResource( nil, rsrc ); i++ )
                            tmpSrc[ i ] = ptr[ i ] + 26;
                        UnlockResource( hdl );
                    }
                }
            }
            rezLoaded = true;
#else
            // Need to define for other platforms?
#endif
        }
        memcpy( tmp, tmpSrc, sizeof( tmp ) );

        if( countDown <= 0 )
        {
            y = 4 + yOff - eOffset;
            if( countDown <= -480 )
            {
                tmp[ ( (-countDown - 480)>> 4 ) + 1 ] = 0;
                drawText.DrawString( 10, y, tmp, fConsoleTextColor );
            }
            y += yOff * ( fNumDisplayLines - ( showTooltip ? 1 : 0 ) );
        }
        else
        {
            for( i = 0, y = 4 + yOff - eOffset, line = fDisplayBuffer; i < fNumDisplayLines - ( showTooltip ? 1 : 0 ); i++ )
            {
                drawText.DrawString( 10, y, line, fConsoleTextColor );
                y += yOff;
                line += kMaxCharsWide;
            }
        }

        if( showTooltip )
            y += yOff;
    }

//  strcpy( tmp, fHelpMode ? "Get Help On:" : "]" );
    if ( fHelpMode )
        strcpy( tmp, "Get Help On:");
    else if (fPythonMode )
        if ( fPythonMultiLines == 0 )
            strcpy( tmp, ">>>");
        else
            strcpy( tmp, "...");
    else
        strcpy( tmp, "]" );

    drawText.DrawString( 10, y, tmp, 255, 255, 255, 255 );
    i = 19 + drawText.CalcStringWidth( tmp );
    drawText.DrawString( i, y, fWorkingLine, fConsoleTextColor );

    if( fCursorTicks >= 0 )
    {
        strcpy( tmp, fWorkingLine );
        tmp[ fWorkingCursor ] = 0;
        x = drawText.CalcStringWidth( tmp );
        drawText.DrawString( i + x, y + 2, "_", 255, 255, 255 );
    }
    fCursorTicks--;
    if( fCursorTicks < -kCursorBlinkRate )
        fCursorTicks = kCursorBlinkRate;

    if( showTooltip )
        drawText.DrawString( i, y - yOff, fLastHelpMsg, 255, 255, 0 );
    else
        fHelpTimer--;

    drawText.SetDrawOnTopMode( false );
}

//// IUpdateTooltip //////////////////////////////////////////////////////////

void    pfConsole::IUpdateTooltip( void )
{
    char    tmpStr[ kWorkingLineSize ];
    char    *c;


    strcpy( tmpStr, fWorkingLine );
    c = (char *)fEngine->GetCmdSignature( tmpStr );
    if( c == nil || strcmp( c, fLastHelpMsg ) != 0 )
    {
        /// Different--update timer to wait
        fHelpTimer = kHelpDelay;
        strncpy( fLastHelpMsg, c ? c : "", kMaxCharsWide - 2 );
    }
}

//// IPrintSomeHelp //////////////////////////////////////////////////////////

void    pfConsole::IPrintSomeHelp( void )
{
    char    msg1[] = "The console contains commands arranged under groups and subgroups. \
To use a command, you type the group name plus the command, such as 'Console.Clear' or \
'Console Clear'.";
    
    char    msg2[] = "To get help on a command or group, type '?' followed by the command or \
group name. Typing '?' and just hitting enter will bring up this message. Typing '?' and \
then 'commands' will bring up a list of all base groups and commands.";

    char    msg3[] = "You can also have the console auto-complete a command by pressing tab. \
This will search for a group or command that starts with what you have typed. If there is more \
than one match, pressing tab repeatedly will cycle through all the matches.";


    AddLine( "" );
    AddLine( "How to use the console:" );
    IAddParagraph( msg1, 2 );
    AddLine( "" );
    IAddParagraph( msg2, 2 );
    AddLine( "" );
    IAddParagraph( msg3, 2 );
    AddLine( "" );
}


//============================================================================
void pfConsole::AddLineF(const char * fmt, ...) {
    char str[1024];
    va_list args;
    va_start(args, fmt);
    hsVsnprintf(str, arrsize(str), fmt, args);
    va_end(args);
    AddLine(str);
}

//============================================================================
void pfConsole::RunCommandAsync (const char cmd[]) {

    plConsoleMsg * consoleMsg = new plConsoleMsg;
    consoleMsg->SetCmd(plConsoleMsg::kExecuteLine);
    consoleMsg->SetString(cmd);
//  consoleMsg->SetBreakBeforeDispatch(true);
    consoleMsg->Send(nil, true);
}