/* ----------------------------------------------------------------------
    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 10: Particles
------------------------------------------------------------------------

    Overview:
    This tutorial introduces ParticleMgr and ParticleEmitter with a small
    emitter that follows the mouse and one-shot effects on click.

    Key concepts:
    - Create a ParticleEmitter through ParticleMgr.
    - Apply built-in particle presets.
    - Bind a Physics collision set and select its particle response.
    - Update particles at the fixed logic rate.
    - Render particles from the screen render callback.
    - Spawn one-shot particle effects.

    Controls:
    - Mouse: Move the active emitter.
    - Left Click: Spawn an explosion one-shot.
    - 1..5: Switch particle preset.
    - O: Cycle particle collider mode.
    - D: Toggle debug window.
    - Grave: Toggle console.
    - Q / ESC: Quit.

    Loop order:
    - Process input events.
    - Run one fixed logic update.
    - Update ParticleMgr with the logic delta time.
    - Let Main::update() call the screen render callback.
------------------------------------------------------------------------ */

#include "CRM64Pro.h"
#include "RunParameter.h"

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 RESOURCE_BG_CDC_NAME "Tutorial_bg"
#define RESOURCE_CURSOR_CDC_NAME "Tutorial_cursor"
#define OUTPUT_CDC OUTPUTDIR"Tutorial.cdc"
#define TUTORIAL_COLLIDER_X      (320.0f)
#define TUTORIAL_COLLIDER_Y      (200.0f)
#define TUTORIAL_COLLIDER_W      (320.0f)
#define TUTORIAL_COLLIDER_H      (20.0f)

static const Sint32 iLogicRate = 60;
static const Sint32 iRenderRate = 0;
static const Sint32 iScreenW = 960;
static const Sint32 iScreenH = 540;

struct TutorialPreset
{
    eParticleEffectPreset ePreset;
    const char* szName;
};

static const TutorialPreset g_Presets[] =
{
    { PEP_FIRE, "Fire" },
    { PEP_SMOKE, "Smoke" },
    { PEP_SNOW, "Snow" },
    { PEP_SPARKS, "Sparks" },
    { PEP_SPELL_AURA, "Spell aura" }
};

struct TutorialState
{
    Sint32 idBgImage = -1;
    Sint32 idCursor = -1;
    Sint32 idBuiltInFont = -1;
    Sint32 idDebugWindow = -1;
    Sint32 idEmitter = -1;
    Sint32 idCollisionSet = -1;
    float fMouseX = iScreenW * 0.5f;
    float fMouseY = iScreenH * 0.5f;
    Sint32 iPreset = 0;
    Sint32 iCurrentRenderRate = 0;
    Sint32 iCurrentLogicRate = 0;
    Sint32 iLogicFrames = 0;
    eParticleCollisionMode eCollisionMode = PCM_NONE;
};

// Record a successful tutorial task.
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");
}

// Get the particle emitter used by the tutorial.
static ParticleEmitter* getEmitter(TutorialState& rState)
{
    return Main::instance().particleMgr().get(rState.idEmitter);
}

// Return the display name for one particle collision mode.
static const char* getCollisionModeName(eParticleCollisionMode eMode)
{
    switch(eMode)
    {
    case PCM_NONE: return "NONE";
    case PCM_KILL: return "KILL";
    case PCM_STOP: return "STOP";
    case PCM_BOUNCE: return "BOUNCE";
    default: return "UNKNOWN";
    }
}

// Cycle the tutorial particle collision response.
static bool cycleCollisionMode(TutorialState& rState)
{
    eParticleCollisionMode eMode;
    switch(rState.eCollisionMode)
    {
    case PCM_NONE: eMode = PCM_KILL; break;
    case PCM_KILL: eMode = PCM_STOP; break;
    case PCM_STOP: eMode = PCM_BOUNCE; break;
    default: eMode = PCM_NONE; break;
    }

    ParticleEmitter* pEmitter = getEmitter(rState);
    if(!pEmitter || !pEmitter->setCollisionMode(eMode, 0.75f)) return false;
    rState.eCollisionMode = eMode;
    return true;
}

// Apply the selected particle preset.
static bool applyPreset(TutorialState& rState, Sint32 iPreset)
{
    if(iPreset < 0 || iPreset >= static_cast<Sint32>(sizeof(g_Presets) / sizeof(g_Presets[0]))) return false;

    ParticleEmitter* pEmitter = getEmitter(rState);
    if(!pEmitter) return false;

    if(!pEmitter->applyPreset(g_Presets[iPreset].ePreset)
       || !pEmitter->setMaxParticles(1600)
       || !pEmitter->setPosition(rState.fMouseX, rState.fMouseY)
       || !pEmitter->setCollisionSet(rState.idCollisionSet)
       || !pEmitter->setCollisionMode(rState.eCollisionMode, 0.75f)
       || !pEmitter->play()) return false;

    rState.iPreset = iPreset;
    return true;
}

// Update the tutorial state for the current frame.
static void updateLogic(TutorialState& rState, float fDeltaTime)
{
    ParticleEmitter* pEmitter = getEmitter(rState);
    if(pEmitter) pEmitter->setPosition(rState.fMouseX, rState.fMouseY);
    Main::instance().particleMgr().update(fDeltaTime);
    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 the fixed particle collider used by the tutorial.
static void renderCollider(Main& mC64, eParticleCollisionMode eMode)
{
    Uint8 iRed = 180;
    Uint8 iGreen = 180;
    Uint8 iBlue = 180;
    if(eMode == PCM_KILL) { iRed = 255; iGreen = 64; iBlue = 64; }
    else if(eMode == PCM_STOP) { iRed = 255; iGreen = 220; iBlue = 64; }
    else if(eMode == PCM_BOUNCE) { iRed = 64; iGreen = 180; iBlue = 255; }
    const float fX2 = TUTORIAL_COLLIDER_X + TUTORIAL_COLLIDER_W;
    const float fY2 = TUTORIAL_COLLIDER_Y + TUTORIAL_COLLIDER_H;
    mC64.gfx().line(TUTORIAL_COLLIDER_X, TUTORIAL_COLLIDER_Y, fX2, TUTORIAL_COLLIDER_Y,
        iRed, iGreen, iBlue, 255);
    mC64.gfx().line(fX2, TUTORIAL_COLLIDER_Y, fX2, fY2, iRed, iGreen, iBlue, 255);
    mC64.gfx().line(fX2, fY2, TUTORIAL_COLLIDER_X, fY2, iRed, iGreen, iBlue, 255);
    mC64.gfx().line(TUTORIAL_COLLIDER_X, fY2, TUTORIAL_COLLIDER_X, TUTORIAL_COLLIDER_Y,
        iRed, iGreen, iBlue, 255);
}

// Draw the tutorial status information.
static void renderHUD(Font* pFont, TutorialState& rState)
{
    if(!pFont) return;

    ParticleEmitter* pEmitter = getEmitter(rState);
    char szLine[160];
    snprintf(szLine, sizeof(szLine), "Tutorial 10: Particles   Render: %d fps   Logic: %d fps",
        rState.iCurrentRenderRate, rState.iCurrentLogicRate);
    renderText(pFont, 10, 10, szLine);

    snprintf(szLine, sizeof(szLine), "Preset: %s   Live: %d   Collider: %s",
        g_Presets[rState.iPreset].szName, pEmitter ? pEmitter->getLiveCount() : 0,
        getCollisionModeName(rState.eCollisionMode));
    renderText(pFont, 10, 32, szLine);

    renderText(pFont, 10, 510, "Mouse moves emitter   Left click explosion   1..5 preset   O collider   D debug");
}

// Draw the tutorial frame for the current state.
static void renderFrame(Image* pBgImage, Font* pFont, TutorialState& rState)
{
    if(pBgImage) pBgImage->render();

    Main& mC64 = Main::instance();
    mC64.gfx().drawGlow(rState.fMouseX, rState.fMouseY, 96.0f, 255, 150, 40, 120, 0.55f);
    mC64.particleMgr().render();
    renderCollider(mC64, rState.eCollisionMode);
    renderHUD(pFont, rState);
}

// Adapt the renderer to the engine callback signature.
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* pFont = mC64.fontMgr().get(pState->idBuiltInFont);
    renderFrame(pBgImage, pFont, *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();
}

// 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;

    CMem::setStatsLevel(CMem::MSL_NORMAL);
    atexit(CMem::destroy);

    Main& mC64 = Main::instance();
    Log& mLog = *mC64.logMgr().get();
    TutorialState state;

    mLog.init("Tutorial_10_Particles", LL_DEBUG, LM_FILE | LM_STDOUT, OUTPUTDIR"Tutorial_10_Particles.log");

    mLog.msg(LL_INFO, "\nSETUP: screen and timing\n");
    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);

    mC64.configMgr().setMTFriendly(1);

    Screen* pScreen = mC64.configMgr().get();
    pScreen->setSize(iScreenW, iScreenH);
    pScreen->setDriver(eRenderer);
    pScreen->setTitle("CRM64Pro - Tutorial 10: Particles");

    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: load from CDC\n");
    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 built-in 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);

    mLog.msg(LL_INFO, "\nPARTICLES\n");
    mLog.msg(LL_INFO, "  Create emitter ... ");
    state.idEmitter = mC64.particleMgr().create("TutorialMouseEmitter");
    state.idCollisionSet = mC64.physics().createCollisionSet("TutorialParticleCollider");
    const Sint32 idCollider = mC64.physics().addColliderRectangle(state.idCollisionSet,
        TUTORIAL_COLLIDER_X, TUTORIAL_COLLIDER_Y, TUTORIAL_COLLIDER_W, TUTORIAL_COLLIDER_H);
    if(state.idEmitter <= 0 || state.idCollisionSet <= 0 || idCollider <= 0
        || getEmitter(state) == nullptr || !applyPreset(state, 0))
    {
        logTaskFailed(mLog, state.idEmitter);
        Main::terminate();
        return -1;
    }
    logTaskOk(mLog);

    mLog.msg(LL_INFO, "\nTOOLS: debug window and console\n");
    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("Preset", &state.iPreset);
    pDebug->addWatch("Render fps", &state.iCurrentRenderRate);
    pDebug->addWatch("Logic fps", &state.iCurrentLogicRate);
    pDebug->addWatch("Logic frames", &state.iLogicFrames);
    pDebug->baseWidget().hide();
    logTaskOk(mLog);

    Console* pConsole = mC64.guiMgr().getConsole();
    if(pConsole) pConsole->print("Tutorial 10: Particles 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, "  [Mouse] Move particle emitter\n");
    mLog.msg(LL_INFO, "  [Left Click] Spawn explosion one-shot\n");
    mLog.msg(LL_INFO, "  [1..5] Switch preset\n");
    mLog.msg(LL_INFO, "  [O] Cycle collider mode: NONE / KILL / STOP / BOUNCE\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)
    {
        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_1 && event.key.key <= SDLK_5)
                {
                    const Sint32 iPreset = static_cast<Sint32>(event.key.key - SDLK_1);
                    if(applyPreset(state, iPreset))
                        mLog.msg(LL_INFO, "  Preset: %s\n", g_Presets[state.iPreset].szName);
                    else
                        mLog.msg(LL_ERROR, "  Preset change failed\n");
                }
                else if(event.key.key == SDLK_D)
                {
                    toggleDebugWindow(pDebug);
                }
                else if(event.key.key == SDLK_O && cycleCollisionMode(state))
                {
                    mLog.msg(LL_INFO, "  Collider: %s\n", getCollisionModeName(state.eCollisionMode));
                }
            }
            else if(event.type == SDL_EVENT_MOUSE_MOTION)
            {
                state.fMouseX = event.motion.x;
                state.fMouseY = event.motion.y;
            }
            else if(event.type == SDL_EVENT_MOUSE_BUTTON_DOWN && event.button.button == SDL_BUTTON_LEFT)
            {
                const Sint32 idOneShot = mC64.particleMgr().playOneShot(
                    PEP_EXPLOSION, event.button.x, event.button.y);
                ParticleEmitter* pOneShot = mC64.particleMgr().get(idOneShot);
                if(pOneShot)
                {
                    pOneShot->setCollisionSet(state.idCollisionSet);
                    pOneShot->setCollisionMode(state.eCollisionMode, 0.75f);
                }
            }
        }

        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, mC64.getLogicDeltaTime());
    }

    mC64.particleMgr().info();
    mLog.msg(LL_INFO, "\nCLEANUP\n");
    mLog.msg(LL_INFO, "  Close particle emitters ... ");
    mC64.particleMgr().close(0);
    logTaskOk(mLog);
    mLog.msg(LL_INFO, "  Close particle collider ... ");
    if(mC64.physics().closeCollisionSet(state.idCollisionSet) < 0) logTaskFailed(mLog);
    else logTaskOk(mLog);
    mLog.msg(LL_INFO, "  Close debug window ... ");
    mC64.guiMgr().close(state.idDebugWindow);
    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;
}
