/* ----------------------------------------------------------------------
    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 11: Lightmap
------------------------------------------------------------------------

    Overview:
    This tutorial introduces the GFX 2D lightmap pass. It draws a scene first,
    accumulates lights and occluders into a lightmap, then applies the result
    over the rendered scene.

    Key concepts:
    - Start a lightmap pass with GFX::beginLighting().
    - Add rectangle, circle and segment occluders.
    - Draw point lights and spot lights.
    - Use GFXLight for candle animation and color/radius changes.
    - Finish the pass with GFX::endLighting().
    - Keep HUD rendering outside the lighting pass.

    Controls:
    - Mouse: Move the warm point light.
    - L: Toggle lightmap pass.
    - O: Toggle occluders.
    - P: Cycle ambient presets.
    - A / Z: Change ambient intensity.
    - I / K: Change mouse-light intensity.
    - D: Toggle debug window.
    - Grave: Toggle console.
    - Q / ESC: Quit.

    Loop order:
    - Process input events.
    - Run one fixed logic update.
    - Render the scene.
    - Render and apply the lightmap.
    - Render the HUD.
------------------------------------------------------------------------ */

#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"

static const Sint32 iLogicRate = 20;
static const Sint32 iRenderRate = 0;
static const Sint32 iScreenW = 960;
static const Sint32 iScreenH = 540;

struct AmbientPreset
{
    const char* szName;
    Uint8 iR, iG, iB;
    float fIntensity;
};

static const AmbientPreset vAmbientPresets[] =
{
    { "Daylight",      255, 255, 255, 1.00f },
    { "Neutral dim",   255, 255, 255, 0.25f },
    { "Dusk",          154, 168, 200, 0.45f },
    { "Moonlight",     113, 136, 184, 0.20f },
    { "Warm interior", 208, 144,  88, 0.20f },
    { "Torch",          58,  32,   8, 0.08f },
    { "Blackout",        0,   0,   0, 0.00f }
};

static const Sint32 iAmbientPresetCount = static_cast<Sint32>(sizeof(vAmbientPresets) / sizeof(vAmbientPresets[0]));

struct TutorialState
{
    Sint32 idBgImage = -1;
    Sint32 idCursor = -1;
    Sint32 idBuiltInFont = -1;
    Sint32 idDebugWindow = -1;
    float fMouseX = iScreenW * 0.5f;
    float fMouseY = iScreenH * 0.5f;
    Sint32 iAmbientPreset = 1;
    float fAmbientIntensity = 0.25f;
    float fMouseIntensity = 1.0f;
    Sint32 iCurrentRenderRate = 0;
    Sint32 iCurrentLogicRate = 0;
    Sint32 iLogicFrames = 0;
    bool bLighting = true;
    bool bOccluders = true;
};

// 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");
}

// Convert a light value to a valid colour byte.
static Uint8 lightByte(float fValue)
{
    if(fValue <= 0.0f) return 0;
    if(fValue >= 255.0f) return 255;
    return static_cast<Uint8>(fValue + 0.5f);
}

// 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 the scene objects lit by the lightmap.
static void renderSceneProps(GFX& mGFX)
{
    mGFX.rectFilled(260, 205, 390, 295, 62, 76, 88, 255);
    mGFX.rect(260, 205, 390, 295, 210, 220, 230, 255);
    mGFX.circleFilled(610, 350, 56, 56, 72, 74, 255);
    mGFX.circle(610, 350, 56, 210, 220, 230, 255);
    mGFX.line(485, 130, 760, 260, 210, 220, 230, 255);
}

// Draw and apply the lightmap.
static bool renderLighting(GFX& mGFX, TutorialState& rState)
{
    if(!rState.bLighting) return true;

    const AmbientPreset& Ambient = vAmbientPresets[rState.iAmbientPreset];
    bool bOk = mGFX.beginLighting(lightByte(Ambient.iR * rState.fAmbientIntensity),
        lightByte(Ambient.iG * rState.fAmbientIntensity), lightByte(Ambient.iB * rState.fAmbientIntensity));
    if(!bOk) return false;

    if(rState.bOccluders)
    {
        bOk = mGFX.addLightOccluderRect(260, 205, 390, 295) && bOk;
        bOk = mGFX.addLightOccluderCircle(610, 350, 56) && bOk;
        bOk = mGFX.addLightOccluderSegment(485, 130, 760, 260) && bOk;
    }

    GFXLight CandleLight;
    CandleLight.fX = 175.0f;
    CandleLight.fY = 170.0f;
    CandleLight.fRadius = 165.0f;
    CandleLight.iColor = 0xFFF0B0FF;
    CandleLight.fIntensity = 0.52f;
    CandleLight.bUseOccluders = rState.bOccluders;
    CandleLight.eAnimation = GLA_CANDLE;
    CandleLight.fAnimationRate = 8.0f;
    CandleLight.fAnimationAmount = 0.32f;
    CandleLight.iSecondaryColor = 0xE85018FF;
    CandleLight.fAnimationRadiusAmount = 0.10f;
    CandleLight.iAnimationID = 1;
    bOk = mGFX.drawLight(CandleLight) && bOk;
    GFXLight MouseLight;
    MouseLight.fX = rState.fMouseX;
    MouseLight.fY = rState.fMouseY;
    MouseLight.fRadius = 180.0f;
    MouseLight.iColor = 0xFFDCA0FF;
    MouseLight.fIntensity = rState.fMouseIntensity;
    MouseLight.bUseOccluders = rState.bOccluders;
    bOk = mGFX.drawLight(MouseLight) && bOk;
    GFXLight SpotLight;
    SpotLight.fX = 640.0f;
    SpotLight.fY = 70.0f;
    SpotLight.fRadius = 430.0f;
    SpotLight.fDirection = 90.0f;
    SpotLight.fAngle = 70.0f;
    SpotLight.iColor = 0xB0D8FFFF;
    SpotLight.fIntensity = 0.65f;
    SpotLight.bUseOccluders = rState.bOccluders;
    bOk = mGFX.drawLight(SpotLight) && bOk;
    bOk = mGFX.endLighting() && bOk;
    return bOk;
}

// Draw the tutorial status information.
static void renderHUD(Font* pFont, TutorialState& rState)
{
    if(!pFont) return;

    char szLine[180];
    snprintf(szLine, sizeof(szLine), "Tutorial 11: Lightmap   Render: %d fps   Logic: %d fps",
        rState.iCurrentRenderRate, rState.iCurrentLogicRate);
    renderText(pFont, 10, 10, szLine);

    const AmbientPreset& Ambient = vAmbientPresets[rState.iAmbientPreset];
    snprintf(szLine, sizeof(szLine), "Lighting: %s   Occluders: %s   Ambient: %s #%02X%02X%02X x %.2f -> %d,%d,%d",
        rState.bLighting ? "ON" : "OFF", rState.bOccluders ? "ON" : "OFF", Ambient.szName,
        Ambient.iR, Ambient.iG, Ambient.iB, rState.fAmbientIntensity,
        lightByte(Ambient.iR * rState.fAmbientIntensity), lightByte(Ambient.iG * rState.fAmbientIntensity), lightByte(Ambient.iB * rState.fAmbientIntensity));
    renderText(pFont, 10, 32, szLine);

    snprintf(szLine, sizeof(szLine), "Mouse light %.2f   P preset   A/Z ambient   I/K light   L lighting   O occluders   D debug", rState.fMouseIntensity);
    renderText(pFont, 10, 510, szLine);
    renderText(pFont, 10, 488, "Animated light: GLA_CANDLE; spotlight: above the segment, pointing down with shadow");
}

// Draw the tutorial frame for the current state.
static void renderFrame(Image* pBgImage, Font* pFont, TutorialState& rState)
{
    Main& mC64 = Main::instance();
    GFX& mGFX = mC64.gfx();

    if(pBgImage) pBgImage->render();
    renderSceneProps(mGFX);

    if(!renderLighting(mGFX, rState))
    {
        mGFX.rectFilled(10, 58, 330, 78, 160, 0, 0, 220);
        renderText(pFont, 14, 60, "Lightmap pass failed");
    }

    if(rState.bOccluders)
    {
        mGFX.rect(260, 205, 390, 295, 255, 80, 80, 210);
        mGFX.circle(610, 350, 56, 255, 80, 80, 210);
        mGFX.line(485, 130, 760, 260, 255, 80, 80, 210);
    }

    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_11_Lightmap", LL_DEBUG, LM_FILE | LM_STDOUT, OUTPUTDIR"Tutorial_11_Lightmap.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 11: Lightmap");

    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, "\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("Ambient preset", &state.iAmbientPreset);
    pDebug->addWatch("Ambient intensity", nullptr, &state.fAmbientIntensity);
    pDebug->addWatch("Mouse light intensity", nullptr, &state.fMouseIntensity);
    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 11: Lightmap 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 warm point light\n");
    mLog.msg(LL_INFO, "  [L] Toggle lighting\n");
    mLog.msg(LL_INFO, "  [O] Toggle occluders\n");
    mLog.msg(LL_INFO, "  [P] Cycle ambient presets\n");
    mLog.msg(LL_INFO, "  [A] / [Z] Change ambient intensity\n");
    mLog.msg(LL_INFO, "  [I] / [K] Change mouse-light intensity\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_L) state.bLighting = !state.bLighting;
                else if(event.key.key == SDLK_O) state.bOccluders = !state.bOccluders;
                else if(event.key.key == SDLK_P)
                {
                    state.iAmbientPreset = (state.iAmbientPreset + 1) % iAmbientPresetCount;
                    state.fAmbientIntensity = vAmbientPresets[state.iAmbientPreset].fIntensity;
                }
                else if(event.key.key == SDLK_A)
                {
                    state.fAmbientIntensity += 0.05f;
                    if(state.fAmbientIntensity > 1.0f) state.fAmbientIntensity = 1.0f;
                }
                else if(event.key.key == SDLK_Z)
                {
                    state.fAmbientIntensity -= 0.05f;
                    if(state.fAmbientIntensity < 0.0f) state.fAmbientIntensity = 0.0f;
                }
                else if(event.key.key == SDLK_I)
                {
                    state.fMouseIntensity += 0.05f;
                    if(state.fMouseIntensity > 1.0f) state.fMouseIntensity = 1.0f;
                }
                else if(event.key.key == SDLK_K)
                {
                    state.fMouseIntensity -= 0.05f;
                    if(state.fMouseIntensity < 0.0f) state.fMouseIntensity = 0.0f;
                }
                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);
    }

    mC64.gfx().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 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;
}
