/* ----------------------------------------------------------------------
    CRM64Pro GDK - Roberto Prieto
    Copyright (C) 2013-2026 MegaStorm Systems

    This software is provided 'as-is', without any express or implied
    warranty. In no event will the authors be held liable for any damages
    arising from the use of this software.

    Permission is granted to anyone to use this software for any purpose,
    including commercial applications, and to alter it and redistribute it
    freely, subject to the following restrictions:

    1. The origin of this software must not be misrepresented; you must not
       claim that you wrote the original software.
    2. Altered source versions must be plainly marked as such, and must not be
       misrepresented as being the original software.
    3. This notice may not be removed or altered from any source distribution.

------------------------------------------------------------------------
                        Tutorial 15: Network
------------------------------------------------------------------------

    Overview:
    This tutorial introduces TCP networking with a small command-line chat.
    One process can run as the server and other processes can connect as
    clients. Messages typed in one terminal are sent to the server and then
    broadcast to the other connected clients.

    Key concepts:
    - Initialize NetTCP with NetTCP::init().
    - Create a threaded server with NetTCP::createServer().
    - Connect a client with NetTCP::connectTo().
    - Send user data with NetTCP::sendData().
    - Receive queued network messages with NetTCP::receiveData().
    - Release received payloads with NetTCP::freeData().
    - Request client or locally owned server shutdown.

    Run examples:
    - Terminal 1: Tutorial_15_Network -s
    - Terminal 2: Tutorial_15_Network -c localhost Alice
    - Terminal 3: Tutorial_15_Network -c localhost Bob

    Controls:
    - /quit: Disconnect this client and exit.
    - /serverquit: Stop the server owned by this process (server mode only).
    - /clients: Request fresh client information.
    - /info: Print NetTCP diagnostics.
    - Any other text is sent as a chat message.

    Loop order:
    - Input is read by a small thread so network receive polling keeps running.
    - The main loop polls NetTCP::receiveData().
    - Received payloads are printed and released.
------------------------------------------------------------------------ */

#include "CRM64Pro.h"

#ifdef CRM64PRO_PLATFORM_WINDOWS
#include <conio.h>
#else
#include <sys/select.h>
#include <unistd.h>
#endif

using namespace CRM64Pro;

#ifdef CRM64PRO_PLATFORM_WINDOWS
#define OUTPUTDIR "Win64/"
#elif defined(CRM64PRO_PLATFORM_LINUX)
#define OUTPUTDIR "Linux/"
#elif defined(CRM64PRO_PLATFORM_MACOS)
#define OUTPUTDIR "macOS/"
#endif

#define NETWORK_DEFAULT_HOST "localhost"
#define NETWORK_DEFAULT_NAME "Player"
#define NETWORK_SERVER_NAME "ServerAdmin"
#define NETWORK_PASSWORD "1005"

// Tutorial network settings.
static const Uint16 iServerPort = 2200;
static const Sint32 iInputMaxLen = 256;
static const Sint32 iLoopDelayMS = 20;

// Values shared between the network loop and the input thread.
struct TutorialState
{
    const char* szClientName = NETWORK_DEFAULT_NAME;
    SDL_AtomicInt iRunning;
    bool bServerMode = false;
};

// Helper functions used by setup log messages.
static void logTaskOk(Log& mLog)
{
    mLog.msg(LL_DEBUG, "OK\n");
}

// Record a failed tutorial task.
static void logTaskFailed(Log& mLog, Sint32 iCode)
{
    mLog.msg(LL_ERROR, "FAILED (Code %d)\n", iCode);
}

// Record a failed tutorial task.
static void logTaskFailed(Log& mLog)
{
    mLog.msg(LL_ERROR, "FAILED\n");
}

// Log a failed network operation and return whether it succeeded.
static bool checkNetworkResult(Log& mLog, const char* szAction, eNetResult eResult)
{
    if(eResult == NR_OK) return true;
    mLog.msg(LL_ERROR, "  %s failed (Code %d)\n", szAction, eResult);
    return false;
}

// Check whether the network client is still running.
static bool isRunning(TutorialState& rState)
{
    return SDL_GetAtomicInt(&rState.iRunning) != 0;
}

// SDL_AtomicInt keeps the loop flag safe because it is read by the main
// thread and written by the input thread.
static void setRunning(TutorialState& rState, bool bRunning)
{
    SDL_SetAtomicInt(&rState.iRunning, bRunning ? 1 : 0);
}

// Print the available network tutorial commands.
static void printHelp()
{
    printf("CRM64Pro - Tutorial 15: Network\n\n");
    printf("Run examples:\n");
    printf("  Tutorial_15_Network -s\n");
    printf("  Tutorial_15_Network -c localhost Alice\n");
    printf("  Tutorial_15_Network -c localhost Bob\n\n");
    printf("Commands while connected:\n");
    printf("  /quit       Disconnect this client\n");
    printf("  /serverquit Stop the locally owned server\n");
    printf("  /clients    Request client information\n");
    printf("  /info       Print NetTCP diagnostics\n");
    printf("  text        Send chat message\n\n");
}

// Remove the line ending from console input.
static void trimLineEnd(char* szText)
{
    if(!szText) return;

    // fgets() keeps the line ending. Remove it so commands compare cleanly.
    Sint32 iLen = static_cast<Sint32>(strlen(szText));
    while(iLen > 0 && (szText[iLen - 1] == '\n' || szText[iLen - 1] == '\r'))
    {
        szText[iLen - 1] = '\0';
        iLen--;
    }
}

// Read one complete line from the console.
static bool readConsoleLine(char* szLine, Sint32 iMaxLen)
{
    if(!szLine || iMaxLen <= 1) return false;

#ifdef CRM64PRO_PLATFORM_WINDOWS
    // Windows consoles can be polled with _kbhit(). We build a small edit
    // buffer manually so this function never blocks the input thread.
    static char szEditLine[iInputMaxLen];
    static Sint32 iEditLen = 0;

    while(_kbhit())
    {
        const Sint32 iKey = _getch();
        if(iKey == '\r' || iKey == '\n')
        {
            putchar('\n');
            szEditLine[iEditLen] = '\0';
            snprintf(szLine, static_cast<size_t>(iMaxLen), "%s", szEditLine);
            iEditLen = 0;
            szEditLine[0] = '\0';
            return true;
        }
        if(iKey == '\b')
        {
            if(iEditLen > 0)
            {
                iEditLen--;
                szEditLine[iEditLen] = '\0';
                printf("\b \b");
                fflush(stdout);
            }
            continue;
        }
        if(iKey >= 32 && iKey < 127 && iEditLen < iMaxLen - 1)
        {
            szEditLine[iEditLen++] = static_cast<char>(iKey);
            szEditLine[iEditLen] = '\0';
            putchar(iKey);
            fflush(stdout);
        }
    }
    return false;
#else
    // On Linux and macOS, select() lets us check stdin before calling fgets().
    // This keeps the tutorial loop responsive even when the user is not typing.
    fd_set readfds;
    FD_ZERO(&readfds);
    FD_SET(STDIN_FILENO, &readfds);

    timeval tv;
    tv.tv_sec = 0;
    tv.tv_usec = 0;

    if(select(STDIN_FILENO + 1, &readfds, nullptr, nullptr, &tv) <= 0) return false;
    if(fgets(szLine, static_cast<int>(iMaxLen), stdin) == nullptr) return false;
    trimLineEnd(szLine);
    return true;
#endif
}

// Send a chat message to the connected peers.
static eNetResult sendChatMessage(const char* szClientName, const char* szText)
{
    if(!szClientName || !szText || szText[0] == '\0') return NR_BAD_PARAMETER;

    // NetTCP sends raw bytes. The tutorial formats one text line and sends the
    // string length without the terminating null character.
    char szMessage[iInputMaxLen + 32];
    snprintf(szMessage, sizeof(szMessage), "%s: %s", szClientName, szText);
    return Main::instance().netTCP().sendData(szMessage, static_cast<Sint32>(strlen(szMessage)));
}

// Keep console input separate so receive polling continues while the user types.
static Sint32 inputThread(void* pData)
{
    TutorialState* pState = static_cast<TutorialState*>(pData);
    if(!pState) return -1;

    Log& mLog = *Main::instance().logMgr().get();
    char szLine[iInputMaxLen];

    while(isRunning(*pState))
    {
        // No complete line yet. Sleep briefly so this thread does not burn CPU.
        if(!readConsoleLine(szLine, static_cast<Sint32>(sizeof(szLine))))
        {
            SDL_Delay(iLoopDelayMS);
            continue;
        }
        if(szLine[0] == '\0') continue;

        if(strcmp(szLine, "/quit") == 0)
        {
            // Close this client and exit the tutorial process.
            checkNetworkResult(mLog, "Client close request", Main::instance().netTCP().requestClientClose());
            setRunning(*pState, false);
        }
        else if(strcmp(szLine, "/serverquit") == 0)
        {
            // Only the hosting process owns the server and can stop it.
            // Connected clients receive NM_CLOSE and leave their loops.
            checkNetworkResult(mLog, "Server close request", Main::instance().netTCP().requestServerClose());
        }
        else if(strcmp(szLine, "/clients") == 0)
        {
            // Client information is requested asynchronously. The answer is
            // received later as NM_INFO and read with getClientsInfo().
            checkNetworkResult(mLog, "Client information request", Main::instance().netTCP().queryClientsInfo());
        }
        else if(strcmp(szLine, "/info") == 0)
        {
            Main::instance().netTCP().info();
        }
        else
        {
            if(checkNetworkResult(mLog, "Send message", sendChatMessage(pState->szClientName, szLine)))
            {
                mLog.msg(LL_INFO, "  Sending: %s\n", szLine);
            }
        }
    }

    return 0;
}

// Print the connected client list.
static void printClientTable()
{
    NetTCP::ClientInfo* pClients = nullptr;
    Sint32 iCount = 0;

    // getClientsInfo() reads the latest client table cached by NetTCP.
    Log& mLog = *Main::instance().logMgr().get();
    if(!checkNetworkResult(mLog, "Read client information",
        Main::instance().netTCP().getClientsInfo(&pClients, &iCount)))
    {
        return;
    }

    mLog.msg(LL_INFO, "  Connected clients: %d\n", iCount);
    for(Sint32 i = 0; i < iCount; ++i)
    {
        mLog.msg(LL_INFO, "    [%d] %s  %s  %d ms\n",
            pClients[i].iClientIdx,
            pClients[i].szName,
            pClients[i].ipAddress.szAddress[0] != '\0' ? pClients[i].ipAddress.szAddress : "N/A",
            pClients[i].iLatency);
    }
}

// Handle one message received from the network.
static void handleNetworkMessage(TutorialState& rState, eNetMsg eMsg, void* pData, Uint32 iSize)
{
    Log& mLog = *Main::instance().logMgr().get();

    // receiveData() returns small control messages and optional payloads. The
    // tutorial handles only the messages needed for a basic chat.
    switch(eMsg)
    {
    case NM_CLOSE:
        mLog.msg(LL_INFO, "  Server closed the connection.\n");
        setRunning(rState, false);
        break;

    case NM_NEWCLIENT:
        if(pData && iSize > 0)
            mLog.msg(LL_INFO, "  Client joined: %.*s\n", static_cast<Sint32>(iSize), static_cast<char*>(pData));
        else mLog.msg(LL_INFO, "  Client joined: unknown\n");
        break;

    case NM_QUITCLIENT:
        if(pData && iSize > 0)
            mLog.msg(LL_INFO, "  Client left: %.*s\n", static_cast<Sint32>(iSize), static_cast<char*>(pData));
        else mLog.msg(LL_INFO, "  Client left: unknown\n");
        break;

    case NM_INFO:
        printClientTable();
        break;

    case NM_DATA:
        if(pData && iSize > 0)
        {
            // Payloads are not assumed to be null-terminated. The %.*s format
            // prints exactly the number of bytes received.
            mLog.msg(LL_INFO, "  %.*s\n", static_cast<Sint32>(iSize), static_cast<char*>(pData));
        }
        break;

    // Only servers with an authoritative callback send these replies.
    case NM_DATA_ACCEPTED:
        mLog.msg(LL_DEBUG, "  Message accepted by server.\n");
        break;

    case NM_DATA_DENIED:
        mLog.msg(LL_INFO, "  Message denied by server.\n");
        break;

    case NM_ERROR:
        mLog.msg(LL_ERROR, "  Network error. Closing client loop.\n");
        setRunning(rState, false);
        break;

    default:
        break;
    }
}

// Connect the tutorial client to the server.
static bool connectClient(const char* szHost, TutorialState& rState)
{
    Main& mC64 = Main::instance();
    Log& mLog = *mC64.logMgr().get();

    // The server process also connects a local client, so it can participate
    // in the same chat flow as remote clients.
    mLog.msg(LL_INFO, "  Connect to %s:%d as %s ... ", szHost, iServerPort, rState.szClientName);
    const eNetResult eRet = mC64.netTCP().connectTo(szHost, iServerPort, rState.szClientName, NETWORK_PASSWORD);
    if(eRet != NR_OK)
    {
        logTaskFailed(mLog, eRet);
        return false;
    }
    logTaskOk(mLog);

    // Ask for the initial client list. The result arrives through receiveData().
    checkNetworkResult(mLog, "Initial client information request", mC64.netTCP().queryClientsInfo());
    return true;
}

// Process network activity until the client stops.
static bool runClientLoop(TutorialState& rState)
{
    Main& mC64 = Main::instance();
    Log& mLog = *mC64.logMgr().get();
    SDL_Thread* pInputThread = nullptr;

    mLog.msg(LL_INFO, "\nCHAT READY\n");
    mLog.msg(LL_INFO, "  Type a message and press Enter.\n");
    mLog.msg(LL_INFO, "  Commands: /quit, /serverquit, /clients, /info\n\n");

    // Console input and network receive polling run independently. This keeps
    // incoming messages visible while the user is typing.
    pInputThread = SDL_CreateThread(inputThread, "Tutorial_15_Network_Input", &rState);
    if(!pInputThread)
    {
        mLog.msg(LL_ERROR, "  Could not create input thread. SDL error: %s\n", SDL_GetError());
        setRunning(rState, false);
        return false;
    }

    while(isRunning(rState))
    {
        void* pData = nullptr;
        Uint32 iSize = 0;
        eNetMsg eMsg = NM_NOTHING;

        do
        {
            // Drain all currently queued messages before sleeping. This avoids
            // displaying only one network event per loop tick.
            pData = nullptr;
            iSize = 0;
            eMsg = mC64.netTCP().receiveData(&pData, &iSize);
            if(eMsg != NM_NOTHING && eMsg != NM_PING)
            {
                handleNetworkMessage(rState, eMsg, pData, iSize);
            }
            // Payload memory belongs to NetTCP and must be released after use.
            if(pData) mC64.netTCP().freeData(pData);
        } while(isRunning(rState) && eMsg != NM_NOTHING);

        SDL_Delay(iLoopDelayMS);
    }

    if(pInputThread)
    {
        // Wait for the input thread before closing NetTCP.
        SDL_WaitThread(pInputThread, nullptr);
        pInputThread = nullptr;
    }

    return true;
}

// Run the tutorial application and release its resources.
int main(int argc, char** argv)
{
    // This tutorial does not open a render window, but it still uses the same
    // memory and Main lifetime pattern as the other tutorials.
    CMem::setStatsLevel(CMem::MSL_NORMAL);
    atexit(CMem::destroy);

    Main& mC64 = Main::instance();
    Log& mLog = *mC64.logMgr().get();

    TutorialState state;
    setRunning(state, true);
    const char* szHost = NETWORK_DEFAULT_HOST;

    if(argc < 2)
    {
        printHelp();
        Main::terminate();
        return -1;
    }

    if(strcmp(argv[1], "-s") == 0)
    {
        // Server mode creates the listening server and then joins it as a local
        // client named ServerAdmin.
        state.bServerMode = true;
        state.szClientName = NETWORK_SERVER_NAME;
        mLog.init("Tutorial_15_Network_Server", LL_DEBUG, LM_FILE | LM_STDOUT, OUTPUTDIR"Tutorial_15_Network_Server.log");
    }
    else if(strcmp(argv[1], "-c") == 0 && argc >= 3)
    {
        // Client mode connects to the host passed after -c. The third argument
        // is optional and becomes the displayed chat name.
        state.bServerMode = false;
        szHost = argv[2];
        state.szClientName = (argc >= 4) ? argv[3] : NETWORK_DEFAULT_NAME;
        mLog.init("Tutorial_15_Network_Client", LL_DEBUG, LM_FILE | LM_STDOUT, OUTPUTDIR"Tutorial_15_Network_Client.log");
    }
    else
    {
        printHelp();
        Main::terminate();
        return -1;
    }

    mLog.msg(LL_INFO, "\nCRM64Pro - Tutorial 15: Network\n");

    // NetTCP owns its worker threads and internal queues. Initialize it before
    // creating a server or connecting a client.
    mLog.msg(LL_INFO, "  Initialize NetTCP ... ");
    if(mC64.netTCP().init(LM_FILE) != NR_OK)
    {
        logTaskFailed(mLog);
        Main::terminate();
        return -1;
    }
    logTaskOk(mLog);

    if(state.bServerMode)
    {
        // createServer() starts the threaded TCP server. The password must
        // match the value used by connecting clients.
        mLog.msg(LL_INFO, "  Create threaded server on port %d ... ", iServerPort);
        const eNetResult eRet = mC64.netTCP().createServer(iServerPort, NETWORK_PASSWORD, false);
        if(eRet != NR_OK)
        {
            logTaskFailed(mLog, eRet);
            mC64.netTCP().close();
            Main::terminate();
            return -1;
        }
        logTaskOk(mLog);
    }

    bool bSucceeded = false;
    if(connectClient(szHost, state))
    {
        // From this point, the tutorial runs as a client. In server mode this is
        // the local admin client connected to the server created above.
        bSucceeded = runClientLoop(state);
    }

    mLog.msg(LL_INFO, "\nCLEANUP\n");
    mLog.msg(LL_INFO, "  Close NetTCP ... ");
    const eNetResult eCloseResult = mC64.netTCP().close();
    if(eCloseResult != NR_OK)
    {
        logTaskFailed(mLog, eCloseResult);
        bSucceeded = false;
    }
    else logTaskOk(mLog);

    Main::terminate();
    return bSucceeded ? 0 : -1;
}
