/* ----------------------------------------------------------------------
    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 IslandGame v2 (Free Camera)
------------------------------------------------------------------------

    Overview:
    This tutorial extends IslandGame v1 with two camera modes: follow the
    player or pan the camera manually.

    Key concepts:
    - Load a TMX map with a Scene object factory.
    - Create SceneObject instances from TMX object types.
    - Add a runtime tile layer for terrain weights.
    - Use A* pathfinding with tile costs.
    - Switch between camera follow and manual camera movement.
    - Pick objects and cells from the mouse position.

    Layer layout:
    - Layer 1: Base tile world (camera anchor).
    - Layer 2-4: Additional visual/game layers from TMX.
    - Layer 3 object layer: start/exit gameplay objects.
    - Layer 5: Runtime heightmap/weights debug layer.

    Controls:
    - Right-Click: Move player (A* target cell).
    - C: Toggle camera mode (follow/free).
    - Arrow Keys: Pan camera in free mode.
    - F1: Toggle grid on layer 1.
    - F2: Toggle height/weights debug render.
    - S: Toggle smooth scroll.
    - Q / ESC: Quit.

    Loop order:
    - Poll events and capture mouse button edges.
    - Resolve move targets and click interactions.
    - Run one fixed logic tick via Scene::update().
    - Let Scene update follow mode; pan manually in free mode.
    - 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 = 400;
static const Sint32 iViewH = 300;
static const Sint32 iViewX = (iScreenW - iViewW) / 2;
static const Sint32 iViewY = (iScreenH - iViewH) / 2;

// Logic timing.
static const Sint32 iLogicRate = 20;
static const float fPlayerSpeed = 60.0f;
static const float fCameraPanStep = 5.0f;

struct TutorialState
{
    Scene* pScene = nullptr;
    Sint32 idBgImage = -1;
    Sint32 idDebugFont = -1;
    Sint32 iTileSize = 16;
    bool bCameraFollow = true;
};

static TutorialState* g_pState = nullptr;

// Tile layer used to store pathfinding weights.
// 255 blocks movement, 1 is normal movement, higher values are slower.
class SceneLayerHeightMap : public SceneLayerTile
{
public:
    SceneLayerHeightMap(const string& sName, Sint32 iH, Sint32 iW) : SceneLayerTile(sName, iH, iW)
    {
    }

    // Draw pathfinding weights when the debug layer is visible.
    Sint32 render(Scene* pScene, SceneLayerContext& rContext) override
    {
        if(!g_pState) return 0;

        // Layer 5 owns this runtime weight layer.
        if(!(pScene->getLayerFeatures(5) & SLF_RENDER)) 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++)
        {
            if(iLY < 0 || iLY >= getHeight()) continue;
            // Screen position for this row.
            float fScreenY = rContext.worldToScreen(0.0f, static_cast<float>(iLY * mVR.iTileH), true).y;

            for(Sint32 iLX = mVR.iCXStart; iLX < mVR.iCXEnd; iLX++)
            {
                if(iLX < 0 || iLX >= getWidth()) continue;

                Uint32 iWeight = iCellMap[iLY][iLX];
                // Keep the debug overlay readable.
                if(iWeight == 0) continue;

                // Screen position for this column.
                float fScreenX = rContext.worldToScreen(static_cast<float>(iLX * mVR.iTileW), 0.0f, true).x;

                char szBuf[16];
                if(iWeight == 255) snprintf(szBuf, 16, "X");
                else snprintf(szBuf, 16, "%u", static_cast<unsigned int>(iWeight));

                Sint32 iFW = pFont->getWidth(szBuf);
                Sint32 iFH = pFont->getHeight();

                // Center the text in the tile.
                pFont->setPosition(fScreenX + (mVR.iTileW - iFW) / 2, fScreenY + (mVR.iTileH - iFH) / 2);
                pFont->render(szBuf);
            }
        }
        return 0;
    }
};

// A* pathfinding data.
struct Node
{
    Sint32 iX, iY;
    float  fF; // Total cost
    // Reverse comparison so priority_queue selects the lowest cost first.
    bool operator<(const Node& rOther) const
    {
        return fF > rOther.fF;
    }
};

// Player object.
class Player : public SceneObject
{
public:
    static Sint32 iIDSpriteShared; // Static to share sprite resource across instances
    Sint32 iIDSprite;
    bool   bIsMoving;
    vector<SDL_Point> vPath;   // Grid coordinates path
    Sint32 iCurrentPathIndex;

    Player() : iIDSprite(-1), bIsMoving(false), iCurrentPathIndex(0) {
    }

    void initialize() override
    {
        // Explicitly set size to match the visual sprite (32x32).
        setSize(32.0f, 32.0f);

        // Adjust initial position from TMX anchor (Center-Bottom) to Top-Left
        setPosition(getX() - 16.0f, getY() - 32.0f);

        // Load the shared Sprite resource once.
        if(iIDSpriteShared < 0)
        {
            ImageMgr& mImageMgr = Main::instance().imageMgr();
            SpriteMgr& mSpriteMgr = Main::instance().spriteMgr();
            string sSprName = "PlayerSprite";
            iIDSpriteShared = mSpriteMgr.create(sSprName);
            if(iIDSpriteShared >= 0)
            {
                Sint32 idImg = mImageMgr.loadFromFile(BASEDIR "sprite2.png");
                if(idImg > 0)
                {
                    Sprite* pSpr = mSpriteMgr.get(iIDSpriteShared);
                    pSpr->assignImage(idImg, 1);
                    pSpr->setOffset(32, 32);
                    pSpr->setAnimStateIndex(0, SPR_STATE_UP);
                    pSpr->setAnimStateIndex(1, SPR_STATE_LEFT);
                    pSpr->setAnimStateIndex(2, SPR_STATE_DOWN);
                    pSpr->setAnimStateIndex(3, SPR_STATE_RIGHT);
                    for(Sint32 i = 0; i <= 3; i++) pSpr->setFrameTimeIndex(i, -1, 50);
                    // Initial state: left and paused.
                    pSpr->selectAnim(SPR_STATE_LEFT);
                    pSpr->pause();
                }
            }
        }
        iIDSprite = iIDSpriteShared;
    }

    void updateMovement(float fDeltaTime)
    {
        Sprite* pS = Main::instance().spriteMgr().get(iIDSprite);
        if(!bIsMoving || vPath.empty())
        {
            // Ensure animation is paused when not moving
            if(pS && pS->status() != GS_PAUSED) pS->pause();
            return;
        }

        float fStepRemaining = fPlayerSpeed * fDeltaTime;
        const float fEpsilon = 0.0001f;
        while(bIsMoving && !vPath.empty() && iCurrentPathIndex < (int)vPath.size() && fStepRemaining > fEpsilon)
        {
            // Convert grid point to world center aligned with Object Center
            // Target is Top-Left of the sprite area.
            float fNextWorldX = (float)(vPath[iCurrentPathIndex].x * g_pState->iTileSize) + (g_pState->iTileSize * 0.5f) - 16.0f;
            float fNextWorldY = (float)(vPath[iCurrentPathIndex].y * g_pState->iTileSize) + (float)g_pState->iTileSize - 32.0f;

            float fDiffX = fNextWorldX - getX();
            float fDiffY = fNextWorldY - getY();
            const bool bMostlyHorizontal = fabsf(fDiffX) >= fabsf(fDiffY);
            if(bMostlyHorizontal) fDiffY = 0.0f;
            else                  fDiffX = 0.0f;
            float fDist = sqrtf(fDiffX * fDiffX + fDiffY * fDiffY);

            if(fDist <= fEpsilon)
            {
                iCurrentPathIndex++;
                continue;
            }

            if(fDist <= fStepRemaining) // Reach node and keep remaining budget
            {
                setPosition(fNextWorldX, fNextWorldY);
                fStepRemaining -= fDist;
                iCurrentPathIndex++;
                if(iCurrentPathIndex >= (int)vPath.size())
                {
                    bIsMoving = false;
                    vPath.clear();

                    // Pause animation on stop
                    if(pS) pS->pause();
                    return;
                }
                continue;
            }
            // Normalize and move by remaining budget
            float fMoveX = (fDiffX / fDist) * fStepRemaining;
            float fMoveY = (fDiffY / fDist) * fStepRemaining;

            float fNewX = getX() + fMoveX;
            float fNewY = getY() + fMoveY;
            // Keep the orthogonal axis stable to avoid sub-pixel drift on cardinal paths.
            if(bMostlyHorizontal) fNewY = fNextWorldY;
            else                  fNewX = fNextWorldX;
            setPosition(fNewX, fNewY);
            fStepRemaining = 0.0f;

            // Update Animation based on direction
            if(pS)
            {
                Sint32 iState = SPR_STATE_DOWN;
                if(fDiffX > 0) iState = SPR_STATE_RIGHT;
                else if(fDiffX < 0) iState = SPR_STATE_LEFT;
                else if(fDiffY > 0) iState = SPR_STATE_DOWN;
                else if(fDiffY < 0) iState = SPR_STATE_UP;

                // Only change state if different, to avoid resetting frame
                if(pS->getAnimCurrent() != iState || pS->status() == GS_PAUSED)
                {
                    pS->selectAnim(iState);
                    pS->resume();
                }
            }
        }
    }

    void update(Scene*, SceneLayerContext& rContext) override
    {
        updateMovement(rContext.fDeltaTime);
    }

    // Draw the player sprite at the object position.
    void render(Scene* pScene, SceneLayerContext& rContext) override
    {
        if(iIDSprite < 0) return;
        Sprite* pSpr = Main::instance().spriteMgr().get(iIDSprite);
        if(!pSpr) return;

        SDL_FPoint pScreen = rContext.worldToScreen(getX(), getY(), true);
        float fScreenX = pScreen.x;
        float fScreenY = pScreen.y;
        if(rContext.data.Object.bRenderStateValid && rContext.data.Object.pObject == this)
        {
            fScreenX = rContext.data.Object.fScreenX;
            fScreenY = rContext.data.Object.fScreenY;
        }
        pSpr->setPosition(fScreenX, fScreenY);
        pSpr->render();
    }
    void moveTo(Sint32 iGridX, Sint32 iGridY);
};
Sint32 Player::iIDSpriteShared = -1;

// Object factory.
class TutorialObjectFactory : public ISceneObjectFactory
{
public:
    SceneObject* create(const string& rType) override
    {
        if(rType == "start")
        {
            return new(std::nothrow) Player();
        }
        return nullptr; // Return null to let Scene create a generic object
    }
};

// A* implementation.
void Player::moveTo(Sint32 iEndX, Sint32 iEndY)
{
    if(!g_pState || !g_pState->pScene) return;

    // Access HeightMap (Layer 5)
    SceneLayerTile* pMap = g_pState->pScene->accessLayerTile(5);
    if(!pMap) return;

    // Use the center-bottom of the player (the "feet") to determine the current grid cell.
    Sint32 iStartX = (Sint32)((getX() + getWidth() * 0.5f) / static_cast<float>(g_pState->iTileSize));
    Sint32 iStartY = (Sint32)((getY() + getHeight() - 1.0f) / static_cast<float>(g_pState->iTileSize));

    Sint32 iWidth = pMap->getWidth();
    Sint32 iHeight = pMap->getHeight();

    // Bounds check
    if(iStartX < 0 || iStartX >= iWidth || iStartY < 0 || iStartY >= iHeight) return;
    if(iEndX < 0 || iEndX >= iWidth || iEndY < 0 || iEndY >= iHeight) return;

    // Check target walkable (255 = blocked)
    if(pMap->iCellMap[iEndY][iEndX] == 255) return;

    // A* Data
    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 }));

    vOpenSet.push({ iStartX, iStartY, 0.0f });
    vvfCostSoFar[iStartY][iStartX] = 0.0f;

    // 4-Way movement only (No diagonals)
    const Sint32 iDirs[4][2] = { {0,1}, {1,0}, {0,-1}, {-1,0} };

    while(!vOpenSet.empty())
    {
        Node mCurrent = vOpenSet.top();
        vOpenSet.pop();

        if(mCurrent.iX == iEndX && mCurrent.iY == iEndY) break;

        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; // Blocked

                // Distance is always 1.0 for cardinal
                float fDist = 1.0f;
                // Weight 1 = Normal cost. Higher weight = Higher cost.
                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));
                    vOpenSet.push({ iNX, iNY, fPriority });
                    vvCameFrom[iNY][iNX] = { mCurrent.iX, mCurrent.iY };
                }
            }
        }
    }

    // Reconstruct
    vPath.clear();
    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());
        iCurrentPathIndex = 0;
        bIsMoving = true;

        // Ensure animation starts
        Sprite* pS = Main::instance().spriteMgr().get(iIDSprite);
        if(pS) pS->resume();
    }
}

// Setup function.
bool setupTutorial()
{
    if(!g_pState) return false;
    Main& mC64 = Main::instance();
    Log& mLog = *mC64.logMgr().get();

    // 1. Set Object factory and load TMX in one go
    static TutorialObjectFactory Factory;
    string sTMX = BASEDIR "tiled/rpg/island.tmx";

    Sint32 idScene = mC64.sceneMgr().loadFromFile(sTMX, &Factory);
    if(idScene < 0)
    {
        mLog.msg(LL_CRITICAL, "Failed to load TMX: %s\n", sTMX.c_str());
        return false;
    }

    // Retrieve the engine pointer from the Manager
    g_pState->pScene = mC64.sceneMgr().get(idScene);
    if(!g_pState->pScene) return false;

    // 2. Create Layer 5 (HeightMap) from Layer 1 dimensions.
    SceneLayerTile* pLayer1 = g_pState->pScene->accessLayerTile(1);
    if(!pLayer1)
    {
        mLog.msg(LL_ERROR, "Required tile layer 1 was not found.\n");
        return false;
    }

    // Use tile size from loaded layer 1 as source of truth.
    const Sint32 iMapW = pLayer1->getWidth();
    const Sint32 iMapH = pLayer1->getHeight();
    g_pState->iTileSize = pLayer1->getCellWidth();
    if(iMapW <= 0 || iMapH <= 0 || g_pState->iTileSize <= 0)
    {
        mLog.msg(LL_ERROR, "Tile layer 1 has invalid dimensions.\n");
        return false;
    }

    SceneLayerHeightMap* pLayerHM = new(std::nothrow) SceneLayerHeightMap("HeightMap", iMapH, iMapW);
    if(!pLayerHM)
    {
        mLog.msg(LL_ERROR, "Failed to allocate the heightmap layer.\n");
        return false;
    }
    if(!pLayerHM->setCellWidth(g_pState->iTileSize) ||
        !pLayerHM->setCellHeight(g_pState->iTileSize))
    {
        delete pLayerHM;
        mLog.msg(LL_ERROR, "Failed to configure the heightmap cell size.\n");
        return false;
    }

    // Add to Engine at index 5
    if(g_pState->pScene->addLayer(pLayerHM, 5) < 0)
    {
        delete pLayerHM;
        mLog.msg(LL_ERROR, "Failed to add the heightmap layer.\n");
        return false;
    }

    // Default: Updates enabled (for logic access), Rendering disabled (F2 to toggle) 
    if(!g_pState->pScene->setLayerFeatures(5, SLF_UPDATE, true) ||
        !g_pState->pScene->setLayerFeatures(5, SLF_RENDER, false) ||
        !g_pState->pScene->setLayerAlphaMod(5, 128))
    {
        mLog.msg(LL_ERROR, "Failed to configure the heightmap layer.\n");
        return false;
    }

    // 3. Load weights from Image
    // "island-heightmap.png" - Black (0) = Blocked, White (255) = Walkable
    string sHMFile = BASEDIR "tiled/rpg/island-heightmap.png";
    Sint32 idImgH = mC64.imageMgr().loadFromFile(sHMFile);
    if(idImgH <= 0)
    {
        mLog.msg(LL_ERROR, "Failed to load Heightmap image: %s\n", sHMFile.c_str());
        return false;
    }

    Image* pHeightImage = mC64.imageMgr().get(idImgH);
    SDL_Surface* sH = pHeightImage ? pHeightImage->getSurface() : nullptr;
    if(!sH || sH->w <= 0 || sH->h <= 0)
    {
        mC64.imageMgr().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))
            {
                mC64.imageMgr().close(idImgH);
                mLog.msg(LL_ERROR, "Failed to read Heightmap image pixels.\n");
                return false;
            }

            const Sint32 iBrightness = (iR + iG + iB) / 3;

            // Black is blocked; brighter pixels have lower movement costs.
            Uint32 iWeight = 255;
            if(iBrightness > 10)
            {
                iWeight = 255 - iBrightness;
                if(iWeight < 1) iWeight = 1;
            }
            pLayerHM->setCellValue(iY, iX, iWeight);
        }
    }
    mC64.imageMgr().close(idImgH);

    return true;
}

// Adapt the renderer to the engine callback signature.
Sint32 renderWrapper(Sint32)
{
    if(!g_pState || !g_pState->pScene) return 0;

    // Draw background first
    if(g_pState->idBgImage > 0)
    {
        Image* pBG = Main::instance().imageMgr().get(g_pState->idBgImage);
        if(pBG) pBG->render();
    }

    // Then Scene
    g_pState->pScene->render();
    return 0;
}

// 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.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. Init
    CMem::setStatsLevel(CMem::MSL_HIGH);
    atexit(CMem::destroy);

    Main& mC64 = Main::instance();
    TutorialState state;
    g_pState = &state;
    mC64.logMgr().get()->init("Tutorial_16_Scene_IslandGame_v2", LL_INFO, LM_FILE | LM_STDOUT, OUTPUTDIR"Tutorial_16_Scene_IslandGame_v2.log");
    if(!mC64.timer().init() || !mC64.timer().setRate(0, iLogicRate))
    {
        mC64.logMgr().get()->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 - Island Game v2");
    if(pScreen->show() < 0)
    {
        Main::terminate();
        return -1;
    }
    if(!pScreen->setRenderCallback(renderWrapper))
    {
        mC64.logMgr().get()->msg(LL_ERROR, "Failed to set the render callback.\n");
        Main::terminate();
        return -1;
    }

    // Load the background and debug font.
    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)
    {
        mC64.logMgr().get()->msg(LL_ERROR, "Failed to load the background or debug font.\n");
        closeTutorial();
        return -1;
    }

    // 2. Setup
    if(!setupTutorial())
    {
        closeTutorial();
        return -1;
    }

    // Find Player object (Layer 3)
    Player* pPlayer = nullptr;
    SceneObject* pExit = nullptr;

    if(g_pState->pScene->isObjectLayer(3))
    {
        SceneLayerObject* pLayerObj = g_pState->pScene->accessLayerObject(3);
        if(pLayerObj)
        {
            pPlayer = dynamic_cast<Player*>(pLayerObj->findFirstByType("start"));
            pExit = pLayerObj->findFirstByType("exit");
        }
    }

    if(!pPlayer)
    {
        mC64.logMgr().get()->msg(LL_ERROR, "Player object ('start') not found in Layer 3.\n");
        closeTutorial();
        return -1;
    }
    if(!pExit)
    {
        mC64.logMgr().get()->msg(LL_INFO, "Exit area object ('exit') not found in Layer 3.\n");
    }

    // Camera setup
    bool bShowGrid = false;
    bool bShowWeights = false;
    bool bSmoothScroll = true;

    // Viewport & Camera Initialization
    SDL_Rect rView = { iViewX, iViewY, iViewW, iViewH };
    if(!g_pState->pScene->setViewport(&rView))
    {
        mC64.logMgr().get()->msg(LL_ERROR, "Failed to configure the Scene viewport.\n");
        closeTutorial();
        return -1;
    }
    mC64.logMgr().get()->msg(LL_INFO, "Init: Viewport set to [%d, %d, %d, %d]\n", rView.x, rView.y, rView.w, rView.h);

    // Camera setup (follow by default)
    SceneCameraParams mCam;
    mCam.bClampToBounds = true;
    mCam.fDamping = 0.0f;
    mCam.ptTargetAnchor = { 0.5f, 0.5f };
    mCam.ptScreenAnchor = { 0.5f, 0.5f };
    if(!g_pState->pScene->setCameraTarget(1, pPlayer) ||
        !g_pState->pScene->setCameraParams(1, mCam) ||
        !g_pState->pScene->setCameraMode(1, SCM_SNAP))
    {
        mC64.logMgr().get()->msg(LL_ERROR, "Failed to configure the Scene camera.\n");
        closeTutorial();
        return -1;
    }

    for(Sint32 i = 1; i <= 4; i++)
    {
        if(!g_pState->pScene->setLayerFeatures(i, SLF_SMOOTHSCROLL, bSmoothScroll))
        {
            mC64.logMgr().get()->msg(LL_ERROR, "Failed to enable smooth scrolling on layer %d.\n", i);
            closeTutorial();
            return -1;
        }
    }

    mC64.logMgr().get()->msg(LL_INFO, "Initialization complete. Starting loop.\n");
    mC64.logMgr().get()->msg(LL_INFO, "\nCONTROLS:\n [Right-Click] Move\n [Cursors] Scroll with Camera mode\n [C] Switch to Camera mode\n [S] Toggle smooth scroll\n [F1] Toggle grid\n [F2] Toggle weights\n [Q/Esc] Quit\n\n");

    SDL_Event ev;
    bool bDone = false;
    while(!bDone)
    {
        while(mC64.update(&ev))
        {
            if(ev.type == SDL_EVENT_QUIT) bDone = true;
            if(ev.type == SDL_EVENT_KEY_DOWN)
            {
                if(ev.key.key == SDLK_ESCAPE || ev.key.key == SDLK_Q) bDone = true;
                if(ev.key.key == SDLK_C)
                {
                    g_pState->bCameraFollow = !g_pState->bCameraFollow;
                    if(g_pState->bCameraFollow)
                    {
                        if(pPlayer) g_pState->pScene->setCameraTarget(1, pPlayer);
                        g_pState->pScene->setCameraMode(1, SCM_SNAP);
                    }
                    else
                    {
                        g_pState->pScene->setCameraMode(1, SCM_MANUAL);
                    }
                }
                if(ev.key.key == SDLK_F1)
                {
                    bShowGrid = !bShowGrid;
                    g_pState->pScene->setDebugOverlay(SDO_TILE_CELLGRID, bShowGrid);
                }
                if(ev.key.key == SDLK_F2)
                {
                    bShowWeights = !bShowWeights;
                    g_pState->pScene->setLayerFeatures(5, SLF_RENDER, bShowWeights);
                }
                if(ev.key.key == SDLK_S)
                {
                    bSmoothScroll = !bSmoothScroll;
                    for(Sint32 i = 1; i <= 4; i++) g_pState->pScene->setLayerFeatures(i, SLF_SMOOTHSCROLL, bSmoothScroll);
                }
            }
        }
        if(bDone) break;

        // Input
        Sint32 iMouseB = mC64.cursorMgr().getButtons();
        Sint32 iMousePrev = mC64.cursorMgr().getButtonsPrev();
        if((iMouseB & SDL_BUTTON_MASK(SDL_BUTTON_RIGHT)) && !(iMousePrev & SDL_BUTTON_MASK(SDL_BUTTON_RIGHT)) && pPlayer)
        {
            Sint32 iGX = 0;
            Sint32 iGY = 0;
            if(g_pState->pScene->mouseToCell(&iGX, &iGY, 1))
            {
                mC64.logMgr().get()->msg(LL_INFO, "Move to: Cell(%d, %d)\n", iGX, iGY);
                pPlayer->moveTo(iGX, iGY);
                g_pState->bCameraFollow = true;
                g_pState->pScene->setCameraTarget(1, pPlayer);
                g_pState->pScene->setCameraMode(1, SCM_SNAP);
            }
        }

        float fMX = mC64.cursorMgr().getX();
        float fMY = mC64.cursorMgr().getY();
        float fWX = 0.0f, fWY = 0.0f;
        vector<SceneObject*> vHits;

        // Precise hit testing
        if(g_pState->pScene->screenToWorld((Sint32)fMX, (Sint32)fMY, &fWX, &fWY, 3))
        {
            vector<SceneObject*> vQuery;
            SceneLayerObject* pLayerObj = g_pState->pScene->accessLayerObject(3);
            if(pLayerObj) pLayerObj->queryRegion(fWX, fWY, 1.0f, 1.0f, vQuery);

            for(auto* pObj : vQuery)
            {
                if(pObj->containsPoint(fWX, fWY)) vHits.push_back(pObj);
            }
        }

        // Update cursor appearance
        if(!vHits.empty()) mC64.cursorMgr().select(SDL_SYSTEM_CURSOR_POINTER);
        else               mC64.cursorMgr().select(SDL_SYSTEM_CURSOR_DEFAULT);

        // Handle left-click interaction
        if((iMouseB & SDL_BUTTON_MASK(SDL_BUTTON_LEFT)) && !(iMousePrev & SDL_BUTTON_MASK(SDL_BUTTON_LEFT)))
        {
            if(!vHits.empty())
            {
                SceneObject* pObj = vHits.front();
                mC64.logMgr().get()->msg(LL_INFO, "Hit object: '%s' [%s]\n",
                    pObj->getName().c_str(), pObj->getType().c_str());
            }
        }

        g_pState->pScene->update();

        // Manual/free camera mode
        if(!g_pState->bCameraFollow)
        {
            float fDX = 0.0f;
            float fDY = 0.0f;
            if(mC64.getKeyState(SDLK_LEFT))  fDX -= fCameraPanStep;
            if(mC64.getKeyState(SDLK_RIGHT)) fDX += fCameraPanStep;
            if(mC64.getKeyState(SDLK_UP))    fDY -= fCameraPanStep;
            if(mC64.getKeyState(SDLK_DOWN))  fDY += fCameraPanStep;
            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));
                }
            }
        }

        // Exit area logic
        if(pPlayer && pExit && pPlayer->overlapsObject(pExit))
        {
            pPlayer->bIsMoving = false;
            pPlayer->vPath.clear();

            g_pState->pScene->pause();

            if(mC64.tool().messageBox("Exit Island", "Do you want to leave the island and end the game?", MBB_YES | MBB_NO, MBT_QUESTION) == MBB_YES)
            {
                bDone = true;
            }
            else
            {
                pPlayer->setPosition(pPlayer->getX(), pPlayer->getY() + 32.0f);
            }
            g_pState->pScene->resume();
        }
    }

    g_pState->pScene->save(OUTPUTDIR"islandgame_v2.tmx", "", "islandgame.cdc", true);
    mC64.sceneMgr().info();
    closeTutorial();
    return 0;
}
