/* ----------------------------------------------------------------------
    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 16 Scene WarGame
------------------------------------------------------------------------

    Overview:
    This tutorial loads a strategy map, assigns terrain costs, moves a unit
    with A* pathfinding, renders the unit, and updates fog of war.

    Key concepts:
    - Build terrain costs from a heightmap image.
    - Use A* pathfinding on a weighted grid.
    - Convert between screen, world and cell coordinates.
    - Render sprites from a layer callback.
    - Reveal fog of war around the moving unit.
    - Move a unit at a fixed logic rate.

    Layer layout:
    - Layer 1: Visual background.
    - Layer 2: Logic heightmap/weights.
    - Layer 3: Sprite placement layer.
    - Layer 4: Fog of war layer.

    Controls:
    - Right-Click: Move player unit.
    - Cursor Keys: Manual camera scroll.
    - F1: Toggle grid + heightmap debug.
    - F2: Toggle fog of war.
    - Q / ESC: Quit.

    Loop order:
    - Poll events and capture input state.
    - Apply camera scroll input.
    - Resolve right-click target cell and path request.
    - Run fixed-step movement/fog updates.
    - Render from callback using Scene::render().
------------------------------------------------------------------------ */

#include "CRM64Pro.h"
#include "RunParameter.h"


using namespace CRM64Pro;

// Paths used by the tutorial.
#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_BG_CDC_NAME "Tutorial_bg"
#define OUTPUT_CDC OUTPUTDIR"Tutorial.cdc"

// Screen and viewport size.
static const Sint32 iScreenW = 960;
static const Sint32 iScreenH = 540;
static const Sint32 iViewW = 640;
static const Sint32 iViewH = 480;
static const Sint32 iViewX = (iScreenW - iViewW) / 2;
static const Sint32 iViewY = (iScreenH - iViewH) / 2;

// Logic timing.
static const Sint32 iLogicRate = 20;
static const float fLogicStep = 1.0f / static_cast<float>(iLogicRate);
static const float fScrollSpeed = 320.0f;          // Pixels per second
static const float fPlayerSpeed = 80.0f;           // Pixels per second

// Player unit state.
// Grid coordinates drive pathfinding; offsets draw smooth movement between cells.
struct Player
{
    Sint32 iIDSprite;
    Sint32 iX, iY;             // Grid coordinates (cells)
    float  fOffX, fOffY;       // Visual pixel offset from center of current cell
    Sint32 iTargetX, iTargetY; // Destination (grid cells)
    bool   bIsMoving;
    vector<SDL_Point> vPath;   // Path to follow (list of grid points)
    Sint32 iCurrentPathIndex;  // Current step in the path
};

// State shared by callbacks.
struct TutorialState
{
    Player* pPlayer = nullptr;
    Sint32 idBgImage = -1;
    Sint32 idDebugFont = -1;
    Sint32 iFogFirstGID = 0; // GID for semi-transparent fog. GID+1 is opaque.
    Sint32 iTileSize = 0; // Determined at runtime from map
    Scene* pScene = nullptr;
};

static TutorialState* g_pState = nullptr;

// Screen render callback.
Sint32 renderWrapper(Sint32)
{
    if(!g_pState || !g_pState->pScene) return 0;
    Main& mC64 = Main::instance();

    Image* pBG = mC64.imageMgr().get(g_pState->idBgImage);
    if(pBG) pBG->render();
    g_pState->pScene->render();

    return 0;
}

// Tile layer used to show terrain weights.
class SceneLayerHeightMap : public SceneLayerTile
{
public:
    SceneLayerHeightMap(const string& sName, Sint32 iH, Sint32 iW) : SceneLayerTile(sName, iH, iW)
    {
    }

    // Draw terrain weights on top of visible cells.
    Sint32 render(Scene* pScene, SceneLayerContext& rContext) override
    {
        if(!g_pState) return 0;
        Font* pFont = Main::instance().fontMgr().get(g_pState->idDebugFont);
        if(!pFont) return 0;

        // Only draw cells inside the current viewport.
        VisibleRange mVR = calculateVisibleRange(rContext.rViewport, rContext.fScrollX, rContext.fScrollY);

        for(Sint32 iLY = mVR.iCYStart; iLY < mVR.iCYEnd; iLY++)
        {
            Sint32 iCY = iLY;
            if(iCY < 0 || iCY >= getHeight()) continue;

            // Convert the row position to screen coordinates.
            float fScreenY = rContext.worldToScreen(0.0f, static_cast<float>(iLY * mVR.iTileH), true).y;

            for(Sint32 iLX = mVR.iCXStart; iLX < mVR.iCXEnd; iLX++)
            {
                Sint32 iCX = iLX;
                if(iCX < 0 || iCX >= getWidth()) continue;

                Uint32 iWeight = iCellMap[iCY][iCX];

                // Ensure x position includes scroll offset
                float fRawX = rContext.worldToScreen(static_cast<float>(iLX * mVR.iTileW), 0.0f, true).x;
                float fScreenX = floorf(fRawX);

                char szBuf[16];
                snprintf(szBuf, 16, "%d", iWeight);
                Sint32 iFW = pFont->getWidth(szBuf);
                Sint32 iFH = pFont->getHeight();

                pFont->setPosition(fScreenX + (mVR.iTileW - iFW) / 2, fScreenY + (mVR.iTileH - iFH) / 2);
                pFont->render(szBuf);
            }
        }
        return 0;
    }
};

// --- A* pathfinder ---
// Standard A* implementation for grid-based pathfinding with weights.
struct Node
{
    Sint32 iX, iY;
    float  fF, fG; // f = total cost (g + h), g = path cost from start
    // Reverse comparison so priority_queue selects the lowest cost first.
    bool operator<(const Node& rOther) const
    {
        return fF > rOther.fF;
    }
};

// Find a walkable route between two map cells.
vector<SDL_Point> findPath(Sint32 iStartX, Sint32 iStartY, Sint32 iEndX, Sint32 iEndY)
{
    vector<SDL_Point> vPath;
    if(!g_pState || !g_pState->pScene) return vPath;

    SceneLayerTile* pMap = g_pState->pScene->accessLayerTile(2); // Layer 2 is heightmap
    if(!pMap) return vPath;

    Sint32 iWidth = pMap->getWidth();
    Sint32 iHeight = pMap->getHeight();

    // Validation: bounds and target walkable check
    if(iStartX < 0 || iStartX >= iWidth || iStartY < 0 || iStartY >= iHeight ||
        iEndX < 0 || iEndX >= iWidth || iEndY < 0 || iEndY >= iHeight) return vPath;

    // Check if target is unwalkable (255)
    if(pMap->iCellMap[iEndY][iEndX] == 255) return vPath;

    // Data structures for A*
    priority_queue<Node> vOpenSet;
    vector<vector<float>> vvfCostSoFar(iHeight, vector<float>(iWidth, -1.0f));
    vector<vector<SDL_Point>> vvCameFrom(iHeight, vector<SDL_Point>(iWidth, { -1, -1 }));

    // Start node
    vOpenSet.push({ iStartX, iStartY, 0.0f, 0.0f });
    vvfCostSoFar[iStartY][iStartX] = 0.0f;

    // Directions (8-way movement: cardinal + diagonals)
    const Sint32 iDirs[8][2] = { {0,1}, {1,0}, {0,-1}, {-1,0}, {1,1}, {1,-1}, {-1,1}, {-1,-1} };

    while(!vOpenSet.empty())
    {
        Node mCurrent = vOpenSet.top();
        vOpenSet.pop();

        if(mCurrent.iX == iEndX && mCurrent.iY == iEndY) break; // Path found!

        for(auto& rDir : iDirs)
        {
            Sint32 iNX = mCurrent.iX + rDir[0];
            Sint32 iNY = mCurrent.iY + rDir[1];

            if(iNX >= 0 && iNX < iWidth && iNY >= 0 && iNY < iHeight)
            {
                Uint32 iWeight = pMap->iCellMap[iNY][iNX];
                if(iWeight == 255) continue; // Unwalkable terrain

                // Cost: grid distance (1.0 or 1.414 for diagonal) * terrain weight
                float fDist = (rDir[0] != 0 && rDir[1] != 0) ? 1.414f : 1.0f;
                float fNewCost = vvfCostSoFar[mCurrent.iY][mCurrent.iX] + (fDist * (float)iWeight);

                if(vvfCostSoFar[iNY][iNX] == -1.0f || fNewCost < vvfCostSoFar[iNY][iNX])
                {
                    vvfCostSoFar[iNY][iNX] = fNewCost;
                    float fPriority = fNewCost + (abs(iEndX - iNX) + abs(iEndY - iNY)); // Manhattan distance heuristic
                    vOpenSet.push({ iNX, iNY, fPriority, fNewCost });
                    vvCameFrom[iNY][iNX] = { mCurrent.iX, mCurrent.iY };
                }
            }
        }
    }

    // Reconstruct path by backtracking from end to start
    if(vvCameFrom[iEndY][iEndX].x != -1)
    {
        SDL_Point mCurr = { iEndX, iEndY };
        while(mCurr.x != iStartX || mCurr.y != iStartY)
        {
            vPath.push_back(mCurr);
            mCurr = vvCameFrom[mCurr.y][mCurr.x];
        }
        reverse(vPath.begin(), vPath.end());
    }

    return vPath;
}

// Update fog of war.
// Resets visibility and calculates new visible area based on player position.
void updateFog(int iGridX, int iGridY)
{
    if(!g_pState || !g_pState->pScene) return;

    SceneLayerTile* pFog = g_pState->pScene->accessLayerTile(4);
    if(!pFog) return;

    Sint32 iW = pFog->getWidth();
    Sint32 iH = pFog->getHeight();

    // 1. Downgrade currently visible cells (0) to explored (g_pState->iFogFirstGID). 
    // Using full map scan (Option A) which is sufficient for this map size.
    for(Sint32 iY = 0; iY < iH; iY++)
    {
        for(Sint32 iX = 0; iX < iW; iX++)
        {
            // If cell is currently visible (0), fade it to semi-transparent (explored)
            if(pFog->iCellMap[iY][iX] == 0)
            {
                pFog->iCellMap[iY][iX] = g_pState->iFogFirstGID;
            }
        }
    }

    // 2. Reveal new cells (circular radius)
    Sint32 iRadius = 3;
    Sint32 iR2 = iRadius * iRadius;

    for(Sint32 iY = -iRadius; iY <= iRadius; iY++)
    {
        for(Sint32 iX = -iRadius; iX <= iRadius; iX++)
        {
            // Check circular distance
            if(iX * iX + iY * iY <= iR2)
            {
                Sint32 iGX = iGridX + iX;
                Sint32 iGY = iGridY + iY;

                if(iGX >= 0 && iGX < iW && iGY >= 0 && iGY < iH)
                {
                    pFog->iCellMap[iGY][iGX] = 0; // Set to 0 (No Tile) = Fully Visible
                }
            }
        }
    }
}

// Update sprite position on layer 3 (Logical grid update)
// This keeps the "Entities" tile layer in sync with where units are logically.
void updateSpriteMap(Sint32 iOldX, Sint32 iOldY, Sint32 iNewX, Sint32 iNewY)
{
    if(!g_pState || !g_pState->pScene) return;
    SceneLayerTile* pSprLayer = g_pState->pScene->accessLayerTile(3);
    if(!pSprLayer) return;

    // Clear old position
    if(iOldX >= 0 && iOldX < pSprLayer->getWidth() && iOldY >= 0 && iOldY < pSprLayer->getHeight())
        pSprLayer->setCellValue(iOldY, iOldX, 0);

    // Set new position
    if(iNewX >= 0 && iNewX < pSprLayer->getWidth() && iNewY >= 0 && iNewY < pSprLayer->getHeight())
        pSprLayer->setCellValue(iNewY, iNewX, 1000); // Mark presence (value > 0)
}

// Callback: layer 3 render
// Renders the sprite with sub-pixel precision using the offset values.
void onRenderSprites(const SceneLayerContext& rContext)
{
    if(!g_pState || !g_pState->pPlayer) return;
    Sprite* pSpr = Main::instance().spriteMgr().get(g_pState->pPlayer->iIDSprite);
    if(!pSpr) return;

    // Calculate absolute world position using Grid + Sub-cell Offset
    float fWorldX = (static_cast<float>(g_pState->pPlayer->iX * g_pState->iTileSize)) + g_pState->pPlayer->fOffX;
    float fWorldY = (static_cast<float>(g_pState->pPlayer->iY * g_pState->iTileSize)) + g_pState->pPlayer->fOffY;

    // Apply scroll and viewport offset to get screen coordinates
    SDL_FPoint pScreen = rContext.worldToScreen(fWorldX, fWorldY, true);
    float fScreenX = pScreen.x;
    float fScreenY = pScreen.y;

    // Optimization: scissor test to avoid rendering off-screen sprites
    if(fScreenX > rContext.rViewport.x - 64 && fScreenX < rContext.rViewport.x + rContext.rViewport.w &&
        fScreenY > rContext.rViewport.y - 64 && fScreenY < rContext.rViewport.y + rContext.rViewport.h)
    {
        pSpr->setPosition(fScreenX, fScreenY);
        pSpr->render();
    }
}

// Set up sprite animations.
// Configures the sprite sheet and defines the animation states.
void setupSpriteAnimations(Sint32 iIDSpr)
{
    Sprite* pSpr = Main::instance().spriteMgr().get(iIDSpr);
    if(!pSpr) return;

    // Configure the sprite sheet layout
    pSpr->setOffset(48, 48);

    // Map logical states to animation rows
    pSpr->setAnimStateIndex(0, SPR_STATE_NORMAL);
    pSpr->setAnimStateIndex(1, SPR_STATE_NORMAL + 1);
    pSpr->setAnimStateIndex(2, SPR_STATE_NORMAL + 2);
    pSpr->setAnimStateIndex(3, SPR_STATE_UP);
    pSpr->setAnimStateIndex(4, SPR_STATE_DOWN);
    pSpr->setAnimStateIndex(5, SPR_STATE_LEFT);
    pSpr->setAnimStateIndex(6, SPR_STATE_RIGHT);
    pSpr->setAnimStateIndex(7, SPR_STATE_UPLEFT);
    pSpr->setAnimStateIndex(8, SPR_STATE_UPRIGHT);
    pSpr->setAnimStateIndex(9, SPR_STATE_DOWNLEFT);
    pSpr->setAnimStateIndex(10, SPR_STATE_DOWNRIGHT);

    // Calculate Hotspot: (48-32)/2 = 8, (48-32) = 8 and frame times
    Sint32 iHX = 8;
    Sint32 iHY = 8;
    for(Sint32 i = 0; i <= 10; i++)
    {
        pSpr->setFrameTimeIndex(i, -1, 150);
        pSpr->setAnimHotSpotIndex(i, iHX, iHY);
    }
    pSpr->setFrameTimeIndex(0, -1, 1000);

    // Set initial state
    pSpr->selectAnim(SPR_STATE_NORMAL);
}

// Set up the Scene architecture.
// Creates the layers and loads resources.
bool setupTutorial(const string& rMapName, Sint32 iTileSize)
{
    if(!g_pState || !g_pState->pScene) return false;

    Log& mLog = *Main::instance().logMgr().get();
    ImageMgr& mImageMgr = Main::instance().imageMgr();
    TileMgr& mTileMgr = Main::instance().tileMgr();

    string sMapFile = rMapName + ".png";
    string sHeightMapFile = rMapName + "-heightmap.png";

    mLog.msg(LL_INFO, "Setting up War Game Layers for map '%s' (TileSize: %d)...\n", rMapName.c_str(), iTileSize);

    // 1. Layer 1: background (visuals)
    if(g_pState->pScene->importLayerFromFile(1, sMapFile, "Background", iTileSize, iTileSize) < 0)
    {
        mLog.msg(LL_ERROR, "Failed to load background map from '%s'.\n", sMapFile.c_str());
        return false;
    }

    // Auto-detect map size in tiles
    SceneLayerTile* pLayerBg = g_pState->pScene->accessLayerTile(1);
    if(!pLayerBg) return false;

    Sint32 iMapW = pLayerBg->getWidth();
    Sint32 iMapH = pLayerBg->getHeight();

    // Get tilesize from the loaded layer (source of truth)
    g_pState->iTileSize = pLayerBg->getCellWidth();
    if(iMapW <= 0 || iMapH <= 0 || g_pState->iTileSize <= 0)
    {
        mLog.msg(LL_ERROR, "Background layer has invalid dimensions.\n");
        return false;
    }

    mLog.msg(LL_INFO, "Layer 1 imported. Map Size: %dx%d tiles. Tile Size: %d.\n", iMapW, iMapH, g_pState->iTileSize);

    // 2. Layer 2: heightmap (logic)
    SceneLayerHeightMap* pLayerH = new(std::nothrow) SceneLayerHeightMap("HeightMap", iMapH, iMapW);
    if(!pLayerH)
    {
        mLog.msg(LL_ERROR, "Failed to allocate the heightmap layer.\n");
        return false;
    }
    if(!pLayerH->setCellWidth(g_pState->iTileSize) ||
       !pLayerH->setCellHeight(g_pState->iTileSize) ||
       g_pState->pScene->addLayer(pLayerH, 2) < 0)
    {
        delete pLayerH;
        mLog.msg(LL_ERROR, "Failed to add the heightmap layer.\n");
        return false;
    }

    if(!g_pState->pScene->setLayerFeatures(2, SLF_RENDER | SLF_UPDATE, false) ||
       !g_pState->pScene->setLayerAlphaMod(2, 128)) // Semi-transparent debug view
    {
        mLog.msg(LL_ERROR, "Failed to configure the heightmap layer.\n");
        return false;
    }

    // Load heightmap data from image
    Sint32 idImgH = mImageMgr.loadFromFile(sHeightMapFile);
    if(idImgH <= 0)
    {
        mLog.msg(LL_ERROR, "Failed to load heightmap image: %s\n", sHeightMapFile.c_str());
        return false;
    }

    Image* pHeightImage = mImageMgr.get(idImgH);
    SDL_Surface* sH = pHeightImage ? pHeightImage->getSurface() : nullptr;
    if(!sH || sH->w <= 0 || sH->h <= 0)
    {
        mImageMgr.close(idImgH);
        mLog.msg(LL_ERROR, "Failed to access heightmap image pixels.\n");
        return false;
    }

    for(Sint32 iY = 0; iY < iMapH; iY++)
    {
        for(Sint32 iX = 0; iX < iMapW; iX++)
        {
            // Sample the center of each tile.
            Sint32 iPX = iX * g_pState->iTileSize + (g_pState->iTileSize / 2);
            Sint32 iPY = iY * g_pState->iTileSize + (g_pState->iTileSize / 2);
            if(iPX >= sH->w) iPX = sH->w - 1;
            if(iPY >= sH->h) iPY = sH->h - 1;

            Uint8 iR = 0, iG = 0, iB = 0, iA = 0;
            if(!SDL_ReadSurfacePixel(sH, iPX, iPY, &iR, &iG, &iB, &iA))
            {
                mImageMgr.close(idImgH);
                mLog.msg(LL_ERROR, "Failed to read heightmap image pixels.\n");
                return false;
            }

            // Red blocks movement; grayscale controls movement cost.
            Uint32 iWeight = 255;
            if(iR <= 200 || iG >= 50 || iB >= 50)
            {
                const Sint32 iBrightness = (iR + iG + iB) / 3;
                iWeight = 128 - static_cast<Uint32>(static_cast<float>(iBrightness) * 0.439f);
                if(iWeight < 16) iWeight = 16;
            }
            pLayerH->setCellValue(iY, iX, iWeight);
        }
    }
    mImageMgr.close(idImgH);

    // 3. Layer 3: sprites (container)
    if(g_pState->pScene->createLayer("Sprites", SLT_TILE, 3, iMapW, iMapH) < 0)
    {
        mLog.msg(LL_ERROR, "Failed to create the sprite layer.\n");
        return false;
    }
    SceneLayerTile* pLayerS = g_pState->pScene->accessLayerTile(3);
    if(!pLayerS || !pLayerS->setCellWidth(g_pState->iTileSize) ||
       !pLayerS->setCellHeight(g_pState->iTileSize) ||
       !g_pState->pScene->setLayerOnRenderEnd(3, onRenderSprites) ||
       !g_pState->pScene->setLayerFeatures(3, SLF_RENDER, true))
    {
        mLog.msg(LL_ERROR, "Failed to configure the sprite layer.\n");
        return false;
    }

    // 4. Layer 4: fog of war
    // Create 2-tile fog system (semi-transparent + opaque)
    Sint32 idFogImg = mImageMgr.create("FogImage");
    SDL_Surface* sFog = SDL_CreateSurface(g_pState->iTileSize * 2, g_pState->iTileSize, SDL_PIXELFORMAT_RGBA8888);
    Image* pFogImage = mImageMgr.get(idFogImg);
    if(idFogImg <= 0 || !pFogImage || !sFog)
    {
        if(sFog) SDL_DestroySurface(sFog);
        mLog.msg(LL_ERROR, "Failed to create the fog image.\n");
        return false;
    }

    // Tile 1: semi-transparent (50% alpha)
    SDL_Rect r1 = { 0, 0, g_pState->iTileSize, g_pState->iTileSize };
    if(!SDL_FillSurfaceRect(sFog, &r1, SDL_MapRGBA(SDL_GetPixelFormatDetails(sFog->format), NULL, 0, 0, 0, 128)))
    {
        SDL_DestroySurface(sFog);
        mLog.msg(LL_ERROR, "Failed to draw the fog image.\n");
        return false;
    }

    // Tile 2: opaque (black 100% alpha)
    SDL_Rect r2 = { g_pState->iTileSize, 0, g_pState->iTileSize, g_pState->iTileSize };
    if(!SDL_FillSurfaceRect(sFog, &r2, SDL_MapRGBA(SDL_GetPixelFormatDetails(sFog->format), NULL, 0, 0, 0, 255)) ||
       pFogImage->assignSurface(sFog, 1) < 0)
    {
        SDL_DestroySurface(sFog);
        mLog.msg(LL_ERROR, "Failed to finish the fog image.\n");
        return false;
    }

    // Create tile resource (2x1 tiles)
    Sint32 idFogTile = mTileMgr.create("FogTiles");
    Tile* pFogTile = mTileMgr.get(idFogTile);
    if(idFogTile <= 0 || !pFogTile || pFogTile->assignImage(idFogImg, 1) < 0 ||
       !pFogTile->setOffset(1, g_pState->iTileSize, g_pState->iTileSize)) // Split into tiles
    {
        mLog.msg(LL_ERROR, "Failed to create the fog tiles.\n");
        return false;
    }

    if(g_pState->pScene->createLayer("FogOfWar", SLT_TILE, 4, iMapW, iMapH) < 0)
    {
        mLog.msg(LL_ERROR, "Failed to create the fog layer.\n");
        return false;
    }

    // This creates GID for tile 1 (semi). GID+1 is tile 2 (opaque).
    g_pState->iFogFirstGID = g_pState->pScene->createTileSet(idFogTile);
    if(g_pState->iFogFirstGID <= 0)
    {
        mLog.msg(LL_ERROR, "Failed to attach the fog tileset.\n");
        return false;
    }

    // Initialize fog with opaque tile (GID + 1)
    SceneLayerTile* pLayerFog = g_pState->pScene->accessLayerTile(4);
    if(!pLayerFog || !pLayerFog->setCellWidth(g_pState->iTileSize) ||
       !pLayerFog->setCellHeight(g_pState->iTileSize))
    {
        mLog.msg(LL_ERROR, "Failed to configure the fog layer.\n");
        return false;
    }
    for(Sint32 iY = 0; iY < iMapH; iY++)
        for(Sint32 iX = 0; iX < iMapW; iX++)
            pLayerFog->setCellValue(iY, iX, g_pState->iFogFirstGID + 1); // Opaque

    if(!g_pState->pScene->setLayerFeatures(4, SLF_RENDER | SLF_UPDATE, true))
    {
        mLog.msg(LL_ERROR, "Failed to enable the fog layer.\n");
        return false;
    }
    return true;
}

// Close resources in ownership order, including partially completed setup.
static void closeTutorial()
{
    Main& mC64 = Main::instance();
    mC64.sceneMgr().close(0);
    mC64.spriteMgr().close(0);
    mC64.tileMgr().close(0);
    mC64.fontMgr().close(0);
    mC64.imageMgr().close(0);
    Main::terminate();
}

// 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;

    // 1. Initialization
    CMem::setStatsLevel(CMem::MSL_HIGH);
    atexit(CMem::destroy);

    Main& mC64 = Main::instance();
    TutorialState state;
    g_pState = &state;
    Player player = {};
    g_pState->pPlayer = &player;

    Log& mLog = *Main::instance().logMgr().get();
    mLog.init("Tutorial_16_Scene_WarGame", LL_DEBUG, LM_FILE | LM_STDOUT, OUTPUTDIR"Tutorial_16_Scene_WarGame.log");
    if(!mC64.timer().init() || !mC64.timer().setRate(0, iLogicRate))
    {
        mLog.msg(LL_ERROR, "Failed to initialize tutorial timing.\n");
        Main::terminate();
        return -1;
    }
    mC64.configMgr().setMTFriendly(1);

    Screen* pScreen = mC64.configMgr().get();
    pScreen->setSize(iScreenW, iScreenH);
    pScreen->setDriver(eRenderer);
    pScreen->setTitle("Tutorial 16 Scene - WarGame");
    if(pScreen->show() < 0)
    {
        Main::terminate();
        return -1;
    }
    if(!pScreen->setRenderCallback(renderWrapper))
    {
        mLog.msg(LL_ERROR, "Failed to set the render callback.\n");
        Main::terminate();
        return -1;
    }

    // 2. Resource loading
    g_pState->idBgImage = mC64.imageMgr().load(OUTPUT_CDC, RESOURCE_BG_CDC_NAME);
    g_pState->idDebugFont = mC64.fontMgr().getBuiltin("CourierNew10White");
    if(g_pState->idBgImage <= 0 || g_pState->idDebugFont <= 0)
    {
        mLog.msg(LL_ERROR, "Failed to load the tutorial interface resources.\n");
        closeTutorial();
        return -1;
    }

    Sint32 idSprImg = mC64.imageMgr().loadFromFile(BASEDIR"sprite.png");
    Image* pImSprite = mC64.imageMgr().get(idSprImg);
    if(idSprImg <= 0 || !pImSprite || pImSprite->setColorKey(255, 255, 255) < 0)
    {
        mLog.msg(LL_ERROR, "Failed to load the player sprite image.\n");
        closeTutorial();
        return -1;
    }

    g_pState->pPlayer->iIDSprite = mC64.spriteMgr().create("Player");
    Sprite* pPlayerSprite = mC64.spriteMgr().get(g_pState->pPlayer->iIDSprite);
    if(!pPlayerSprite || pPlayerSprite->assignImage(idSprImg, 1) < 0)
    {
        mLog.msg(LL_ERROR, "Failed to create the player sprite.\n");
        closeTutorial();
        return -1;
    }
    setupSpriteAnimations(g_pState->pPlayer->iIDSprite);

    // 3. Scene setup
    Sint32 idScene = mC64.sceneMgr().create("WarMap");
    g_pState->pScene = mC64.sceneMgr().get(idScene);
    if(idScene < 0 || !g_pState->pScene)
    {
        mLog.msg(LL_ERROR, "Failed to create the War Game Scene.\n");
        closeTutorial();
        return -1;
    }

    // Prepare all layers and resources
    if(!setupTutorial(BASEDIR"map-war1", 32))
    {
        mLog.msg(LL_CRITICAL, "Failed to setup War Game layers.\n");
        closeTutorial();
        return -1;
    }

    // Initialize player position
    SceneLayerTile* pL1 = g_pState->pScene->accessLayerTile(1);
    if(!pL1)
    {
        mLog.msg(LL_ERROR, "Failed to access the background layer.\n");
        closeTutorial();
        return -1;
    }
    // Convert pixel center to grid center
    g_pState->pPlayer->iX = (Sint32)((pL1->getTotalWidth() / 2.0f) / g_pState->iTileSize);
    g_pState->pPlayer->iY = (Sint32)((pL1->getTotalHeight() / 2.0f) / g_pState->iTileSize);
    g_pState->pPlayer->fOffX = 0.0f;
    g_pState->pPlayer->fOffY = 0.0f;
    g_pState->pPlayer->iTargetX = g_pState->pPlayer->iX;
    g_pState->pPlayer->iTargetY = g_pState->pPlayer->iY;
    g_pState->pPlayer->bIsMoving = false;

    // Set initial position in sprite map
    updateSpriteMap(-1, -1, g_pState->pPlayer->iX, g_pState->pPlayer->iY);

    // 4. Viewport & Camera Initialization
    // We set the Viewport first to define the visible window on the screen.
    // The engine uses this rect to correctly clamp camera movements and calculate 
    // coordinate transformations (Screen -> World).
    SDL_Rect rView = { iViewX, iViewY, iViewW, iViewH };
    if(!g_pState->pScene->setViewport(&rView))
    {
        mLog.msg(LL_ERROR, "Failed to configure the Scene viewport.\n");
        closeTutorial();
        return -1;
    }

    // Position the camera (Layer 1) at the center of the map.
    // Since all layers are added with a parallax of 1.0 (default), they stay synced.
    if(g_pState->pScene->setLayerPosition(1, Position(PH_CENTER), Position(PH_CENTER)) < 0)
    {
        mLog.msg(LL_ERROR, "Failed to center the Scene camera.\n");
        closeTutorial();
        return -1;
    }

    // Initialize fog based on where the player is starting
    updateFog(g_pState->pPlayer->iX, g_pState->pPlayer->iY);

    mLog.msg(LL_INFO, "Initialization complete. Starting loop.\n");
    mLog.msg(LL_INFO, "\nCONTROLS:\n [Right-Click] Move\n [Cursors] Scroll\n [F1] Toggle heightmap\n [F2] Toggle fog of war\n [Q/Esc] Quit\n\n");

    // 5. Game loop
    SDL_Event ev;
    bool bDone = false;
    bool bShowHeightMap = false;
    bool bVisibleFog = true;

    // Accumulator for smooth movement at fixed logic rate
    float fMoveAccum = 0.0f;

    while(!bDone)
    {
        while(mC64.update(&ev))
        {
            switch(ev.type)
            {
            case SDL_EVENT_QUIT:
                bDone = true;
                break;

            case SDL_EVENT_KEY_DOWN:
                if(ev.key.key == SDLK_ESCAPE || ev.key.key == SDLK_Q)
                {
                    bDone = true;
                }
                else if(ev.key.key == SDLK_F1)
                {
                    bShowHeightMap = !bShowHeightMap;
                    g_pState->pScene->setDebugOverlay(SDO_TILE_CELLGRID, bShowHeightMap);
                    g_pState->pScene->setLayerFeatures(2, SLF_RENDER, bShowHeightMap);
                    mLog.msg(LL_INFO, "Heightmap: %s\n", bShowHeightMap ? "ON" : "OFF");
                }
                else if(ev.key.key == SDLK_F2)
                {
                    bVisibleFog = !bVisibleFog;
                    g_pState->pScene->setLayerFeatures(4, SLF_RENDER | SLF_UPDATE, bVisibleFog);
                    mLog.msg(LL_INFO, "Fog of War: %s\n", bVisibleFog ? "ON" : "OFF");
                }
                break;
            }
        }
        if(bDone) break;

        // Scroll layer 1 as all the others are linked (parallax 1.0)
        float fSpeed = fScrollSpeed * fLogicStep;
        float fDX = 0, fDY = 0;

        if(mC64.getKeyState(SDLK_LEFT))  fDX = -fSpeed;
        if(mC64.getKeyState(SDLK_RIGHT)) fDX = fSpeed;
        if(mC64.getKeyState(SDLK_UP))    fDY = -fSpeed;
        if(mC64.getKeyState(SDLK_DOWN))  fDY = fSpeed;
        if(fDX != 0.0f || fDY != 0.0f)
        {
            float fX = 0.0f, fY = 0.0f;
            if(g_pState->pScene->getLayerPosition(1, &fX, &fY) >= 0)
            {
                g_pState->pScene->setLayerPosition(1, Position(fX + fDX), Position(fY + fDY));
            }
        }

        const Sint32 iMouseButtons = mC64.cursorMgr().getButtons();
        const Sint32 iMouseButtonsPrev = mC64.cursorMgr().getButtonsPrev();
        const bool bRightPressed =
            ((iMouseButtons & SDL_BUTTON_MASK(SDL_BUTTON_RIGHT)) != 0) &&
            ((iMouseButtonsPrev & SDL_BUTTON_MASK(SDL_BUTTON_RIGHT)) == 0);
        if(bRightPressed) // Right-click edge handling
        {
            Sint32 iTX = 0;
            Sint32 iTY = 0;
            if(g_pState->pScene->mouseToCell(&iTX, &iTY, 1))
            {
                // Only log if target changes
                if(iTX != g_pState->pPlayer->iTargetX || iTY != g_pState->pPlayer->iTargetY)
                {
                    mLog.msg(LL_INFO, "Moving to grid cell: (%d, %d)\n", iTX, iTY);
                }

                // Pathfinding logic: trigger movement only if target changes
                if(iTX != g_pState->pPlayer->iX || iTY != g_pState->pPlayer->iY) {
                    g_pState->pPlayer->vPath = findPath(g_pState->pPlayer->iX, g_pState->pPlayer->iY, iTX, iTY);
                    if(!g_pState->pPlayer->vPath.empty()) {
                        g_pState->pPlayer->iCurrentPathIndex = 0;
                        g_pState->pPlayer->bIsMoving = true;
                    }
                }
            }
        }
        // Process movement based on path or direct target
        if(g_pState->pPlayer->bIsMoving)
        {
            // If we have a path, the immediate target is the first step
            if(g_pState->pPlayer->iCurrentPathIndex < (int)g_pState->pPlayer->vPath.size())
            {
                g_pState->pPlayer->iTargetX = g_pState->pPlayer->vPath[g_pState->pPlayer->iCurrentPathIndex].x;
                g_pState->pPlayer->iTargetY = g_pState->pPlayer->vPath[g_pState->pPlayer->iCurrentPathIndex].y;
            }

            fMoveAccum += fPlayerSpeed * fLogicStep; // pixels accumulated per logic tick

            // Process movement step-by-step
            while(fMoveAccum >= 1.0f)
            {
                // Check if current target is reached (offsets must be zero)
                if(g_pState->pPlayer->iX == g_pState->pPlayer->iTargetX && g_pState->pPlayer->iY == g_pState->pPlayer->iTargetY && g_pState->pPlayer->fOffX == 0.0f && g_pState->pPlayer->fOffY == 0.0f)
                {
                    // Reached intermediate step
                    if(g_pState->pPlayer->iCurrentPathIndex < (int)g_pState->pPlayer->vPath.size())
                    {
                        g_pState->pPlayer->iCurrentPathIndex++; // Advance to next node
                        if(g_pState->pPlayer->iCurrentPathIndex >= (int)g_pState->pPlayer->vPath.size())
                        {
                            g_pState->pPlayer->bIsMoving = false;
                            g_pState->pPlayer->vPath.clear();
                            g_pState->pPlayer->iCurrentPathIndex = 0;
                            fMoveAccum = 0.0f;
                            Sprite* pS = mC64.spriteMgr().get(g_pState->pPlayer->iIDSprite);
                            if(pS) pS->selectAnim(SPR_STATE_NORMAL);
                            break;
                        }
                        else
                        {
                            // Set next target
                            g_pState->pPlayer->iTargetX = g_pState->pPlayer->vPath[g_pState->pPlayer->iCurrentPathIndex].x;
                            g_pState->pPlayer->iTargetY = g_pState->pPlayer->vPath[g_pState->pPlayer->iCurrentPathIndex].y;
                        }
                    }
                    else
                    {
                        g_pState->pPlayer->bIsMoving = false;
                        fMoveAccum = 0.0f;
                        break;
                    }
                }

                // Determine direction vector based on grid target
                float fDiffX = (float)(g_pState->pPlayer->iTargetX - g_pState->pPlayer->iX);
                float fDiffY = (float)(g_pState->pPlayer->iTargetY - g_pState->pPlayer->iY);

                float fMoveX = 0.0f;
                float fMoveY = 0.0f;

                if(fDiffX > 0) fMoveX = 1.0f;
                else if(fDiffX < 0) fMoveX = -1.0f;
                else if(g_pState->pPlayer->fOffX > 0) fMoveX = -1.0f;
                else if(g_pState->pPlayer->fOffX < 0) fMoveX = 1.0f;

                if(fDiffY > 0) fMoveY = 1.0f;
                else if(fDiffY < 0) fMoveY = -1.0f;
                else if(g_pState->pPlayer->fOffY > 0) fMoveY = -1.0f;
                else if(g_pState->pPlayer->fOffY < 0) fMoveY = 1.0f;

                // Apply movement to offset
                g_pState->pPlayer->fOffX += fMoveX;
                g_pState->pPlayer->fOffY += fMoveY;

                // Check thresholds to switch cells (cross half-tile boundary)
                Sint32 iHalfTile = (g_pState->iTileSize / 2) + 1;
                Sint32 iOldX = g_pState->pPlayer->iX;
                Sint32 iOldY = g_pState->pPlayer->iY;
                bool bCellChanged = false;

                if(g_pState->pPlayer->fOffX >= iHalfTile)
                {
                    g_pState->pPlayer->iX++;
                    g_pState->pPlayer->fOffX -= g_pState->iTileSize;
                    bCellChanged = true;
                }
                else if(g_pState->pPlayer->fOffX <= -iHalfTile)
                {
                    g_pState->pPlayer->iX--;
                    g_pState->pPlayer->fOffX += g_pState->iTileSize;
                    bCellChanged = true;
                }

                if(g_pState->pPlayer->fOffY >= iHalfTile)
                {
                    g_pState->pPlayer->iY++;
                    g_pState->pPlayer->fOffY -= g_pState->iTileSize;
                    bCellChanged = true;
                }
                else if(g_pState->pPlayer->fOffY <= -iHalfTile)
                {
                    g_pState->pPlayer->iY--;
                    g_pState->pPlayer->fOffY += g_pState->iTileSize;
                    bCellChanged = true;
                }

                // Snap to center logic (prevent jitter at destination)
                if(g_pState->pPlayer->iX == g_pState->pPlayer->iTargetX && g_pState->pPlayer->iY == g_pState->pPlayer->iTargetY)
                {
                    if(abs(g_pState->pPlayer->fOffX) < 1.0f) g_pState->pPlayer->fOffX = 0.0f;
                    if(abs(g_pState->pPlayer->fOffY) < 1.0f) g_pState->pPlayer->fOffY = 0.0f;
                }

                if(bCellChanged)
                {
                    updateSpriteMap(iOldX, iOldY, g_pState->pPlayer->iX, g_pState->pPlayer->iY);
                    updateFog(g_pState->pPlayer->iX, g_pState->pPlayer->iY);
                }

                // Animation update
                Sint32 iNextState = SPR_STATE_NORMAL;
                if(fMoveX > 0 && fMoveY == 0) iNextState = SPR_STATE_RIGHT;
                else if(fMoveX < 0 && fMoveY == 0) iNextState = SPR_STATE_LEFT;
                else if(fMoveX == 0 && fMoveY > 0) iNextState = SPR_STATE_DOWN;
                else if(fMoveX == 0 && fMoveY < 0) iNextState = SPR_STATE_UP;
                else if(fMoveX > 0 && fMoveY > 0) iNextState = SPR_STATE_DOWNRIGHT;
                else if(fMoveX < 0 && fMoveY > 0) iNextState = SPR_STATE_DOWNLEFT;
                else if(fMoveX > 0 && fMoveY < 0) iNextState = SPR_STATE_UPRIGHT;
                else if(fMoveX < 0 && fMoveY < 0) iNextState = SPR_STATE_UPLEFT;

                Sprite* pS = mC64.spriteMgr().get(g_pState->pPlayer->iIDSprite);
                if(pS) pS->selectAnim(iNextState);

                fMoveAccum -= 1.0f;
            }
        }

        g_pState->pScene->update();
    }

    if(g_pState->pScene->save(OUTPUTDIR"wargame.tmx", "", "wargame.cdc", true) < 0)
    {
        mLog.msg(LL_ERROR, "Failed to save the War Game Scene.\n");
        closeTutorial();
        return -1;
    }
    mC64.sceneMgr().info();
    closeTutorial();
    return 0;
}
