/* ----------------------------------------------------------------------
    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 13: XML
------------------------------------------------------------------------

    Overview:
    This tutorial introduces XML configuration data.
    It loads an external XML file, reads typed attributes and node text, then
    renders those values as text over the background.

    Key concepts:
    - Load an external XML file with XMLMgr::loadFromFile().
    - Get a borrowed XML pointer from XMLMgr.
    - Select nested nodes with XML::nodePointTo().
    - Read string, integer and float attributes.
    - Read node text.
    - Use XML data to configure rendered text and positions.
    - Close XML resources through XMLMgr.

    Controls:
    - D: Toggle debug window.
    - Grave: Toggle console.
    - Q / ESC: Quit.

    Loop order:
    - Process input events.
    - Run one fixed logic update.
    - Let Main::update() call the screen render callback.
    - Render the XML-configured text from the screen callback.
------------------------------------------------------------------------ */

#include "CRM64Pro.h"
#include "RunParameter.h"

using namespace CRM64Pro;

#define BASEDIR "Base/"
#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 RESOURCE_XML_FILE BASEDIR"tutorial.xml"
#define RESOURCE_BG_CDC_NAME "Tutorial_bg"
#define RESOURCE_CURSOR_CDC_NAME "Tutorial_cursor"
#define OUTPUT_CDC OUTPUTDIR"Tutorial.cdc"

// Fixed-rate timing.
// Render rate 0 lets the screen render as often as possible. Logic rate controls counters.
static const Sint32 iLogicRate = 20;
static const Sint32 iRenderRate = 0;

// Window size.
static const Sint32 iScreenW = 960;
static const Sint32 iScreenH = 540;

// Values loaded from XML and changed while the tutorial runs.
struct TutorialState
{
    Sint32 idXML = -1;
    Sint32 idBgImage = -1;
    Sint32 idCursor = -1;
    Sint32 idBuiltInFont = -1;
    Sint32 idDebugWindow = -1;
    string sHeadingText;
    string sIntroText;
    string sPlayerName;
    Sint32 iHeadingX = 48;
    Sint32 iHeadingY = 70;
    Sint32 iIntroX = 48;
    Sint32 iIntroY = 115;
    Sint32 iPlayerScore = 0;
    float fSpawnX = 0.0f;
    float fSpawnY = 0.0f;
    float fMouseX = 0.0f;
    float fMouseY = 0.0f;
    Sint32 iCurrentRenderRate = 0;
    Sint32 iCurrentLogicRate = 0;
    Sint32 iLogicFrames = 0;
};

// Helper functions used by the 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");
}

// Update the tutorial state for the current frame.
static void updateLogic(TutorialState& rState)
{
    rState.iLogicFrames++;
}

// Draw text at the requested screen position.
static void renderText(Font* pFont, Sint32 iX, Sint32 iY, const char* szText)
{
    if(!pFont) return;
    pFont->setPosition(iX, iY);
    pFont->render(szText);
}

// Draw XML-configured text directly over the background.
static void renderFrame(Image* pBgImage, Font* pBuiltInFont, const TutorialState& rState)
{
    if(pBgImage) pBgImage->render();
    if(!pBuiltInFont) return;

    renderText(pBuiltInFont, rState.iHeadingX, rState.iHeadingY, rState.sHeadingText.c_str());
    renderText(pBuiltInFont, rState.iIntroX, rState.iIntroY, rState.sIntroText.c_str());

    char szLine[128];
    snprintf(szLine, sizeof(szLine), "Player name: %s", rState.sPlayerName.c_str());
    renderText(pBuiltInFont, 48, 180, szLine);
    snprintf(szLine, sizeof(szLine), "Score: %d", rState.iPlayerScore);
    renderText(pBuiltInFont, 48, 220, szLine);
    snprintf(szLine, sizeof(szLine), "Spawn position: %.1f, %.1f", rState.fSpawnX, rState.fSpawnY);
    renderText(pBuiltInFont, 48, 260, szLine);
    renderText(pBuiltInFont, 48, 320, "Source: " RESOURCE_XML_FILE);
}

// Screen render callback.
// Main::update() calls this when the screen is ready to render.
static Sint32 renderWrapper(Sint32, void* pObj)
{
    TutorialState* pState = static_cast<TutorialState*>(pObj);
    if(!pState) return 0;

    Main& mC64 = Main::instance();
    Image* pBgImage = mC64.imageMgr().get(pState->idBgImage);
    Font* pBuiltInFont = mC64.fontMgr().get(pState->idBuiltInFont);
    renderFrame(pBgImage, pBuiltInFont, *pState);

    return 0;
}

// Show or hide the debug window.
static void toggleDebugWindow(DebugWindow* pDebug)
{
    if(!pDebug) return;
    if(pDebug->baseWidget().status() == GS_SHOWN) pDebug->baseWidget().hide();
    else pDebug->baseWidget().show();
}

// Load the tutorial data from the xml resource.
static bool loadXMLData(Main& mC64, Log& mLog, TutorialState& rState)
{
    // XMLMgr owns the loaded document. The returned XML pointer is borrowed.
    mLog.msg(LL_INFO, "  Load XML configuration: %s ... ", RESOURCE_XML_FILE);
    rState.idXML = mC64.xmlMgr().loadFromFile(RESOURCE_XML_FILE, "TutorialXMLConfig");
    XML* pXML = mC64.xmlMgr().get(rState.idXML);
    if(rState.idXML < 0 || pXML == nullptr)
    {
        logTaskFailed(mLog, rState.idXML);
        return false;
    }
    logTaskOk(mLog);

    // nodePointTo() selects a nested node by listing its complete path.
    mLog.msg(LL_INFO, "  Read display configuration ... ");
    if(!pXML->nodePointTo(2, "tutorial", "display"))
    {
        logTaskFailed(mLog);
        return false;
    }
    rState.sHeadingText = pXML->getAttributeString("heading", "Tutorial 13: XML");
    rState.iHeadingX = pXML->getAttributeInt("x", rState.iHeadingX);
    rState.iHeadingY = pXML->getAttributeInt("y", rState.iHeadingY);
    logTaskOk(mLog);

    // Node text is useful for configurable rendered text and longer messages.
    mLog.msg(LL_INFO, "  Read text and position ... ");
    if(!pXML->nodePointTo(3, "tutorial", "display", "intro") || !pXML->getText(rState.sIntroText))
    {
        logTaskFailed(mLog);
        return false;
    }
    rState.iIntroX = pXML->getAttributeInt("x", rState.iIntroX);
    rState.iIntroY = pXML->getAttributeInt("y", rState.iIntroY);
    logTaskOk(mLog);

    // Typed accessors convert XML attributes into application data.
    mLog.msg(LL_INFO, "  Read player data ... ");
    if(!pXML->nodePointTo(2, "tutorial", "player"))
    {
        logTaskFailed(mLog);
        return false;
    }
    rState.sPlayerName = pXML->getAttributeString("name", "Player");
    rState.iPlayerScore = pXML->getAttributeInt("score", 0);
    rState.fSpawnX = pXML->getAttributeFloat("spawnX", 0.0f);
    rState.fSpawnY = pXML->getAttributeFloat("spawnY", 0.0f);
    logTaskOk(mLog);

    return true;
}

// Run the tutorial application and release its resources.
int main(int argc, char* argv[])
{
    eConfigRendererDriver eRenderer = CRD_SOFTWARE;
    if(!runParameterReadRender(argc, argv, eRenderer)) return 1;

    // Start CRM64Pro memory tracking before creating engine resources.
    // atexit() releases memory tracking data when the program exits.
    CMem::setStatsLevel(CMem::MSL_NORMAL);
    atexit(CMem::destroy);

    // Main gives access to the CRM64Pro managers.
    Main& mC64 = Main::instance();
    Log& mLog = *mC64.logMgr().get();

    TutorialState state;

    // Write log output to stdout and to a file under validation/bin.
    mLog.init("Tutorial_13_XML", LL_DEBUG, LM_FILE | LM_STDOUT, OUTPUTDIR"Tutorial_13_XML.log");

    mLog.msg(LL_INFO, "\nSETUP: screen and timing\n");

    // Initialize the timer before calling Main::update().
    // setRate() receives render frames per second, then logic frames per second.
    mLog.msg(LL_INFO, "  Initialize timer ... ");
    if(!mC64.timer().init())
    {
        logTaskFailed(mLog);
        Main::terminate();
        return -1;
    }
    logTaskOk(mLog);

    mLog.msg(LL_INFO, "  Set frame rates (render=%d, logic=%d) ... ", iRenderRate, iLogicRate);
    if(!mC64.timer().setRate(iRenderRate, iLogicRate))
    {
        logTaskFailed(mLog);
        Main::terminate();
        return -1;
    }
    logTaskOk(mLog);

    // Add a 1 ms idle delay inside Main::update().
    mC64.configMgr().setMTFriendly(1);

    // Configure and show the default screen.
    Screen* pScreen = mC64.configMgr().get();
    pScreen->setSize(iScreenW, iScreenH);
    pScreen->setDriver(eRenderer);
    pScreen->setTitle("CRM64Pro - Tutorial 13: XML");

    mLog.msg(LL_INFO, "  Create screen (%dx%d) ... ", iScreenW, iScreenH);
    if(pScreen->show() < 0)
    {
        mLog.msg(LL_CRITICAL, "FAILED\n");
        Main::terminate();
        return -1;
    }
    logTaskOk(mLog);

    mLog.msg(LL_INFO, "  Set render callback ... ");
    if(!pScreen->setRenderCallback([&state](Sint32 iMode) { return renderWrapper(iMode, &state); }))
    {
        logTaskFailed(mLog);
        Main::terminate();
        return -1;
    }
    logTaskOk(mLog);

    mLog.msg(LL_INFO, "\nRESOURCES: background, cursor, font and XML\n");

    // Earlier tutorials store the reusable graphics resources in Tutorial.cdc.
    mLog.msg(LL_INFO, "  Load background image from CDC ... ");
    state.idBgImage = mC64.imageMgr().load(OUTPUT_CDC, RESOURCE_BG_CDC_NAME);
    if(state.idBgImage < 0)
    {
        logTaskFailed(mLog, state.idBgImage);
        Main::terminate();
        return -1;
    }
    logTaskOk(mLog);

    mLog.msg(LL_INFO, "  Load cursor from CDC ... ");
    state.idCursor = mC64.cursorMgr().load(OUTPUT_CDC, RESOURCE_CURSOR_CDC_NAME);
    Cursor* pCursor = mC64.cursorMgr().get(state.idCursor);
    if(state.idCursor < 0 || pCursor == nullptr)
    {
        logTaskFailed(mLog, state.idCursor);
        Main::terminate();
        return -1;
    }
    logTaskOk(mLog);

    mLog.msg(LL_INFO, "  Select cursor ... ");
    if(mC64.cursorMgr().select(state.idCursor) < 0 || !mC64.cursorMgr().show())
    {
        logTaskFailed(mLog);
        Main::terminate();
        return -1;
    }
    logTaskOk(mLog);

    mLog.msg(LL_INFO, "  Load content font ... ");
    state.idBuiltInFont = mC64.fontMgr().getBuiltin("CourierNew10White");
    if(state.idBuiltInFont < 0 || mC64.fontMgr().get(state.idBuiltInFont) == nullptr)
    {
        logTaskFailed(mLog, state.idBuiltInFont);
        Main::terminate();
        return -1;
    }
    logTaskOk(mLog);

    if(!loadXMLData(mC64, mLog, state))
    {
        Main::terminate();
        return -1;
    }

    mLog.msg(LL_INFO, "\nTOOLS: debug window and console\n");

    // The DebugWindow shows XML values beside the rendered text.
    mLog.msg(LL_INFO, "  Create debug window ... ");
    state.idDebugWindow = mC64.guiMgr().create("Debug", CRM64PRO_GUI_DEBUGWINDOW);
    DebugWindow* pDebug = mC64.guiMgr().getDebugWindow(state.idDebugWindow);
    if(!pDebug)
    {
        logTaskFailed(mLog, state.idDebugWindow);
        Main::terminate();
        return -1;
    }
    pDebug->baseWidget().setPosition(Position(PH_RIGHT, -120), Position(PH_TOP, 10));
    pDebug->addWatch("Mouse X", nullptr, &state.fMouseX);
    pDebug->addWatch("Mouse Y", nullptr, &state.fMouseY);
    pDebug->addWatch("Score", &state.iPlayerScore);
    pDebug->addWatch("Spawn X", nullptr, &state.fSpawnX);
    pDebug->addWatch("Spawn Y", nullptr, &state.fSpawnY);
    pDebug->addWatch("Render fps", &state.iCurrentRenderRate);
    pDebug->addWatch("Logic fps", &state.iCurrentLogicRate);
    pDebug->addWatch("Logic frames", &state.iLogicFrames);
    pDebug->baseWidget().hide();
    logTaskOk(mLog);

    // Main creates the default Console. Log output is also sent there.
    Console* pConsole = mC64.guiMgr().getConsole();
    if(pConsole) pConsole->print("Tutorial 13: XML ready. Press ` to toggle the console.\n");

    mLog.msg(LL_INFO, "\nCONTROLS\n");
    mLog.msg(LL_INFO, "  [Q] / [ESC] Quit\n");
    mLog.msg(LL_INFO, "  [D] Toggle debug window\n");
    mLog.msg(LL_INFO, "  [`] Toggle console\n");
    mLog.msg(LL_INFO, "\nRUNNING\n");

    SDL_Event event;
    bool bRunning = true;
    while(bRunning)
    {
        // Main::update() processes input, runs internal GUI logic and calls the
        // screen render callback when a render frame is ready.
        while(mC64.update(&event))
        {
            if(event.type == SDL_EVENT_QUIT)
            {
                bRunning = false;
            }
            else if(event.type == SDL_EVENT_KEY_DOWN)
            {
                if(event.key.key == SDLK_Q || event.key.key == SDLK_ESCAPE)
                {
                    bRunning = false;
                }
                else if(event.key.key == SDLK_D)
                {
                    toggleDebugWindow(pDebug);
                }
            }
        }

        if(!bRunning) break;
        state.fMouseX = mC64.cursorMgr().getX();
        state.fMouseY = mC64.cursorMgr().getY();
        state.iCurrentRenderRate = static_cast<Sint32>(mC64.timer().getCurrentRFR());
        state.iCurrentLogicRate = static_cast<Sint32>(mC64.timer().getCurrentLFR());
        updateLogic(state);
    }

    // Close resources owned by this tutorial.
    mC64.xmlMgr().info();
    mLog.msg(LL_INFO, "\nCLEANUP\n");
    mLog.msg(LL_INFO, "  Close debug window ... ");
    mC64.guiMgr().close(state.idDebugWindow);
    logTaskOk(mLog);
    mLog.msg(LL_INFO, "  Close XML documents ... ");
    mC64.xmlMgr().close(0);
    logTaskOk(mLog);
    mLog.msg(LL_INFO, "  Close fonts ... ");
    mC64.fontMgr().close(0);
    logTaskOk(mLog);
    mLog.msg(LL_INFO, "  Close cursors ... ");
    mC64.cursorMgr().close(0);
    logTaskOk(mLog);
    mLog.msg(LL_INFO, "  Close images ... ");
    mC64.imageMgr().close(0);
    logTaskOk(mLog);
    mLog.msg(LL_INFO, "  Close CDC archives ... ");
    mC64.archiveMgr().close(0);
    logTaskOk(mLog);
    Main::terminate();

    return 0;
}
