/* ----------------------------------------------------------------------
    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 Platform
------------------------------------------------------------------------

    Overview:
    This tutorial loads a side-scroller map, moves a player with platform
    physics, checks tile collisions, follows the player with a camera, and
    reacts to a house trigger.

    Key concepts:
    - Load a TMX map with fixed layer roles.
    - Create the player through a Scene object factory.
    - Move a SceneObject with acceleration, gravity and jumping.
    - Check tile collision against a platform layer.
    - Use a Scene camera dead zone.
    - Handle SceneObject trigger callbacks.
    - Pause and resume the Scene around a modal message.

    Layer layout:
    - Loads: Base/tiled/forest/map.tmx
    - Layer 1: Image background.
    - Layer 2: Image background.
    - Layer 3: Platform collision tile layer.
    - Layer 4: Decorative tile layer.
    - Layer 5: Object layer (player + house trigger).

    Controls:
    - Left/Right: Move player.
    - Space: Jump.
    - S: Toggle smooth scroll.
    - P / R: Pause or resume the Scene.
    - D: Toggle object AABB debug rendering.
    - F1: Toggle trigger zone debug rendering.
    - F2: Toggle tile grid debug rendering.
    - F3: Toggle camera dead-zone debug rendering.
    - Q / ESC: Quit.

    Loop order:
    - Poll events and capture input edges/toggles.
    - Run one fixed logic tick via Scene::update().
    - Let Scene update the camera.
    - Show the house prompt after the player stays in the trigger.
    - 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"

static const char* szMapFile = BASEDIR"tiled/forest/map.tmx";

// Screen and viewport size.
static const Sint32 iScreenW = 960;
static const Sint32 iScreenH = 540;
static const Sint32 iViewW = 600;
static const Sint32 iViewH = 250;
static const Sint32 iViewX = (iScreenW - iViewW) / 2;
static const Sint32 iViewY = 0;

// Logic timing.
static const Sint32 iLogicRate = 20;

static const float fPlayerSpeed = 240.0f;
static const float fGroundAccel = 1800.0f;
static const float fAirAccel = 1000.0f;
static const float fGravity = 1400.0f;
static const float fJumpVelocity = -560.0f;
static const float fMaxFallSpeed = 1200.0f;
static const float fMaxStepDown = 8.0f;

static const float fCameraDeadZoneX = 20.0f;
static const float fCameraDeadZoneY = 20.0f;
static const float fHouseCheckX = 24.0f;
static const float fHouseCheckY = 132.0f;
static const float fHouseCheckW = 36.0f;
static const float fHouseCheckH = 33.0f;
static const Sint32 iGroundLayer = 3;
static const Sint32 iPlayerLayer = 5;

struct TutorialState
{
    Scene* pScene = nullptr;
    Sint32 idBgImage = -1;
    bool bJumpQueued = false;

    SceneObject* pPlayer = nullptr;
    SceneLayerTile* pGroundLayer = nullptr;

    float fVelocityX = 0.0f;
    float fVelocityY = 0.0f;
    bool bOnGround = false;

    bool bSmooth = true;
    bool bDebugAABB = false;
    bool bDebugTriggers = false;
    bool bDebugTileGrid = false;
    bool bDebugCameraDeadZone = false;
    bool bHousePromptLock = false;
    bool bHouseTriggerInside = false;
};

static TutorialState* g_pState = nullptr;

// Keep a value inside the given range.
static float clampFloat(float fValue, float fMin, float fMax)
{
    if(fValue < fMin) return fMin;
    if(fValue > fMax) return fMax;
    return fValue;
}

// Move a value toward its target by one step.
static float approach(float current, float target, float delta)
{
    if(current < target) return (current + delta > target) ? target : (current + delta);
    if(current > target) return (current - delta < target) ? target : (current - delta);
    return current;
}

// Enable or disable smooth movement on all layers.
static bool setSmoothForAllLayers(bool bEnable)
{
    if(!g_pState || !g_pState->pScene) return false;
    bool bResult = true;
    const Sint32 iLayerCount = g_pState->pScene->getLayerCount();
    for(Sint32 i = 1; i <= iLayerCount; i++) bResult &= g_pState->pScene->setLayerFeatures(i, SLF_SMOOTHSCROLL, bEnable);
    return bResult;
}

class PlatformPlayer : public SceneObject
{
public:
    void initialize() override
    {
        if(getWidth() <= 0.0f || getHeight() <= 0.0f) setSize(96.0f, 128.0f);
    }

    void update(Scene*, SceneLayerContext& rContext) override
    {
        if(!g_pState) return;
        if(this != g_pState->pPlayer || !g_pState->pGroundLayer) return;

        Main& mC64 = Main::instance();

        const bool bLeft = mC64.getKeyState(SDLK_LEFT);
        const bool bRight = mC64.getKeyState(SDLK_RIGHT);
        const Sint32 iMoveAxis = (bRight ? 1 : 0) - (bLeft ? 1 : 0);

        const float fDT = rContext.fDeltaTime;
        const float fTargetVX = static_cast<float>(iMoveAxis) * fPlayerSpeed;
        const float fAccel = g_pState->bOnGround ? fGroundAccel : fAirAccel;
        g_pState->fVelocityX = approach(g_pState->fVelocityX, fTargetVX, fAccel * fDT);

        if(g_pState->bJumpQueued && g_pState->bOnGround)
        {
            g_pState->fVelocityY = fJumpVelocity;
            g_pState->bOnGround = false;
        }
        g_pState->bJumpQueued = false;

        g_pState->fVelocityY += fGravity * fDT;
        g_pState->fVelocityY = clampFloat(g_pState->fVelocityY, -2000.0f, fMaxFallSpeed);

        const float fWidth = getWidth();
        const float fHeight = getHeight();
        const SDL_FRect rectStart = { getX(), getY(), fWidth, fHeight };
        SDL_FRect rectResolved = rectStart;
        bool bHitX = false;
        bool bHitY = false;
        bool bGroundedSweep = false;
        const bool bSweepOK = mC64.physics().sweepAABBOnLayerTile(
            rectStart,
            g_pState->fVelocityX * fDT,
            g_pState->fVelocityY * fDT,
            g_pState->pGroundLayer,
            &rectResolved,
            &bHitX,
            &bHitY,
            &bGroundedSweep,
            &PlatformPlayer::solidTileForTutorial,
            nullptr
        );

        /*
            Optional one-way vertical behavior (jump-through platforms):
            - Keep normal X collision.
            - Disable Y collision while moving upward (fVelocityY < 0).
            To enable, replace the single sweep above with a split X/Y sweep and
            use the callback below:
                fnYSolid = (fVelocityY < 0) ? nonSolidWhenJumpingUp : solidTileForTutorial;
            Uncomment nonSolidWhenJumpingUp() at the bottom of this class.
        */

        if(!bSweepOK) return;
        if(bHitX) g_pState->fVelocityX = 0.0f;
        if(bHitY) g_pState->fVelocityY = 0.0f;

        bool bGroundedProbe = false;
        if(!bGroundedSweep && fabsf(g_pState->fVelocityY) <= 0.001f)
        {
            bGroundedProbe = mC64.physics().isGroundedOnLayerTile(
                rectResolved,
                g_pState->pGroundLayer,
                fMaxStepDown,
                &PlatformPlayer::solidTileForTutorial,
                nullptr
            );
        }
        g_pState->bOnGround = (bGroundedSweep || bGroundedProbe);
        setPosition(rectResolved.x, rectResolved.y);
    }

private:
    static bool solidTileForTutorial(const SceneLayerTile* pTileLayer, Sint32 iCellX, Sint32 iCellY, Uint32 iCellValue, void*)
    {
        if(!pTileLayer) return false;
        const Sint32 iMapW = pTileLayer->getWidth();
        const Sint32 iMapH = pTileLayer->getHeight();

        if(iCellX < 0 || iCellX >= iMapW) return true;
        if(iCellY >= iMapH) return true;
        if(iCellY < 0) return false;
        return iCellValue != 0;
    }

    // Uncomment to enable one-way vertical behavior (see update() comment above).
    // static bool nonSolidWhenJumpingUp(const SceneLayerTile*, Sint32, Sint32, Uint32, void*)
    // {
    //     return false;
    // }
};

class TutorialObjectFactory : public ISceneObjectFactory
{
public:
    SceneObject* create(const string& rType) override
    {
        if(rType == "hero" || rType == "player") return new(std::nothrow) PlatformPlayer();
        return nullptr;
    }
};

// Adapt the renderer to the engine callback signature.
static Sint32 renderWrapper(Sint32)
{
    if(!g_pState || !g_pState->pScene) return 0;

    if(g_pState->idBgImage > 0)
    {
        Image* pBG = Main::instance().imageMgr().get(g_pState->idBgImage);
        if(pBG) pBG->render();
    }
    g_pState->pScene->render();
    return 0;
}

// Find the player object in a scene layer.
static SceneObject* findPlayerObject(SceneLayerObject* pLayer)
{
    if(!pLayer) return nullptr;
    SceneObject* pObj = pLayer->findFirstByType("hero");
    if(pObj) return pObj;
    pObj = pLayer->findFirstByType("player");
    if(pObj) return pObj;
    pObj = pLayer->findFirstByName("hero");
    if(pObj) return pObj;
    return pLayer->findFirstByName("player");
}

// Find the first object with the requested type.
static SceneObject* findObjectByType(SceneLayerObject* pLayer, const string& rTypeWanted)
{
    if(!pLayer) return nullptr;
    return pLayer->findFirstByType(rTypeWanted);
}

// Check whether an object represents the player.
static bool isPlayerType(const SceneObject* pObj)
{
    if(!pObj) return false;
    const string& rType = pObj->getType();
    const string& sName = pObj->getName();
    return (rType == "hero" || rType == "player" || sName == "hero" || sName == "player");
}

// Handle the player entering the house trigger.
static void onHouseTrigger(const SceneTriggerEvent& ev)
{
    if(!g_pState) return;
    if(!isPlayerType(ev.pOther)) return;

    if(ev.eType == STET_STAY)
    {
        g_pState->bHouseTriggerInside = true;
    }
    else if(ev.eType == STET_EXIT)
    {
        g_pState->bHouseTriggerInside = false;
        g_pState->bHousePromptLock = false;
    }
}

// Process the player response to the house prompt.
static bool handleHousePrompt()
{
    if(!g_pState) return false;
    if(!g_pState->bHouseTriggerInside)
    {
        g_pState->bHousePromptLock = false;
        return false;
    }
    if(g_pState->bHousePromptLock) return false;

    g_pState->bHousePromptLock = true;
    g_pState->fVelocityX = 0.0f;
    g_pState->fVelocityY = 0.0f;

    // Freeze engine/layers before opening a modal dialog.
    g_pState->pScene->pause();

    const bool bExit = (Main::instance().tool().messageBox("House Found", "House found, do you want to exit?", MBB_YES | MBB_NO, MBT_QUESTION) == MBB_YES);
    if(!bExit)
    {
        g_pState->pPlayer->setPosition(g_pState->pPlayer->getX() - 24.0f, g_pState->pPlayer->getY());
    }

    g_pState->pScene->resume();
    return bExit;
}

// Close resources in ownership order, including partially completed setup.
static void closeTutorial()
{
    Main& mC64 = Main::instance();
    mC64.sceneMgr().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;

    CMem::setStatsLevel(CMem::MSL_HIGH);
    atexit(CMem::destroy);

    Main& mC64 = Main::instance();
    TutorialState state;
    g_pState = &state;

    mC64.logMgr().get()->init("Tutorial_16_Scene_Platform", LL_DEBUG, LM_FILE | LM_STDOUT, OUTPUTDIR"Tutorial_16_Scene_Platform.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 - Platform");
    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;
    }
    g_pState->idBgImage = mC64.imageMgr().load(OUTPUT_CDC, RESOURCE_BG_CDC_NAME);
    if(g_pState->idBgImage <= 0)
    {
        mC64.logMgr().get()->msg(LL_ERROR, "Failed to load the tutorial background.\n");
        closeTutorial();
        return -1;
    }

    static TutorialObjectFactory sFactory;
    const Sint32 idScene = mC64.sceneMgr().loadFromFile(szMapFile, &sFactory);
    if(idScene < 0)
    {
        mC64.logMgr().get()->msg(LL_CRITICAL, "Failed to load map: %s\n", szMapFile);
        closeTutorial();
        return -1;
    }

    g_pState->pScene = mC64.sceneMgr().get(idScene);
    if(!g_pState->pScene)
    {
        closeTutorial();
        return -1;
    }

    if(g_pState->pScene->getLayerCount() < 5)
    {
        mC64.logMgr().get()->msg(LL_CRITICAL, "Map must contain at least 5 layers. Found=%d\n", g_pState->pScene->getLayerCount());
        closeTutorial();
        return -1;
    }

    if(g_pState->pScene->getLayerType(1) != SLT_IMAGE || g_pState->pScene->getLayerType(2) != SLT_IMAGE)
    {
        mC64.logMgr().get()->msg(LL_CRITICAL, "Layers 1 and 2 must be image layers.\n");
        closeTutorial();
        return -1;
    }

    if(g_pState->pScene->getLayerType(iGroundLayer) != SLT_TILE)
    {
        mC64.logMgr().get()->msg(LL_CRITICAL, "Layer %d must be a tile layer (platforms).\n", iGroundLayer);
        closeTutorial();
        return -1;
    }

    g_pState->pGroundLayer = g_pState->pScene->accessLayerTile(iGroundLayer);
    if(!g_pState->pGroundLayer)
    {
        mC64.logMgr().get()->msg(LL_CRITICAL, "Missing required tile layer for platforms. ground=%d\n", iGroundLayer);
        closeTutorial();
        return -1;
    }
    if(g_pState->pGroundLayer->getCellWidth() <= 0 || g_pState->pGroundLayer->getCellHeight() <= 0)
    {
        mC64.logMgr().get()->msg(LL_CRITICAL, "Invalid tile size in layer %d.\n", iGroundLayer);
        closeTutorial();
        return -1;
    }

    if(!g_pState->pScene->isObjectLayer(iPlayerLayer))
    {
        mC64.logMgr().get()->msg(LL_CRITICAL, "Layer %d must be an object layer (objects/player).\n", iPlayerLayer);
        closeTutorial();
        return -1;
    }

    SceneLayerObject* pPlayerLayer = g_pState->pScene->accessLayerObject(iPlayerLayer);
    g_pState->pPlayer = findPlayerObject(pPlayerLayer);
    SceneObject* pHouse = findObjectByType(pPlayerLayer, "house");
    if(!g_pState->pPlayer || !pPlayerLayer)
    {
        mC64.logMgr().get()->msg(LL_CRITICAL, "Missing player object layer. player=%d\n", iPlayerLayer);
        closeTutorial();
        return -1;
    }

    if(dynamic_cast<PlatformPlayer*>(g_pState->pPlayer) == nullptr)
    {
        mC64.logMgr().get()->msg(LL_CRITICAL, "Player object must use type 'hero' or 'player' in TMX.\n");
        closeTutorial();
        return -1;
    }

    if(pHouse != nullptr)
    {
        SceneObjectTrigger mHouseTrigger;
        mHouseTrigger.sName = "house_entry";
        mHouseTrigger.ShapeLocal.eType = SST_RECTANGLE;
        mHouseTrigger.ShapeLocal.calculateAABB(fHouseCheckX, fHouseCheckY, fHouseCheckW, fHouseCheckH);
        mHouseTrigger.iTargetLayer = iPlayerLayer;
        mHouseTrigger.iStayDelayMs = 500;
        mHouseTrigger.addIncludeType("player");
        mHouseTrigger.addIncludeType("hero");

        if(!pHouse->addTrigger(mHouseTrigger))
        {
            mC64.logMgr().get()->msg(LL_WARNING, "Failed to add house trigger zone.\n");
        }
        pHouse->setOnTriggerEvent(onHouseTrigger);
    }
    else
    {
        mC64.logMgr().get()->msg(LL_WARNING, "No house object found. House prompt trigger disabled.\n");
    }

    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;
    }

    const float fStartCamX = g_pState->pPlayer->getX() + (g_pState->pPlayer->getWidth() * 0.5f) - (iViewW * 0.5f);
    const float fStartCamY = g_pState->pPlayer->getY() + (g_pState->pPlayer->getHeight() * 0.75f) - (iViewH * 0.5f);

    SceneCameraParams mCam;
    // Dead-zone rectangle size (centered on screen anchor): camera moves only when target leaves this area.
    mCam.rDeadZone = { 0.0f, 0.0f, fCameraDeadZoneX * 2.0f, fCameraDeadZoneY * 2.0f };
    // Follow damping in 1/seconds. Higher values follow faster.
    mCam.fDamping = 12.0f;
    // Clamp camera to map/layer bounds.
    mCam.bClampToBounds = true;
    // Follow point on player: X and Y center
    mCam.ptTargetAnchor = { 0.5f, 0.5f };
    // Screen focus point where target anchor should appear: exact viewport center.
    mCam.ptScreenAnchor = { 0.5f, 0.5f };
    if(!g_pState->pScene->setCameraPosition(iGroundLayer, fStartCamX, fStartCamY, true) ||
        !g_pState->pScene->setCameraTarget(iGroundLayer, g_pState->pPlayer) ||
        !g_pState->pScene->setCameraParams(iGroundLayer, mCam) ||
        !g_pState->pScene->setCameraMode(iGroundLayer, SCM_DEADZONE))
    {
        mC64.logMgr().get()->msg(LL_ERROR, "Failed to configure the Scene camera.\n");
        closeTutorial();
        return -1;
    }

    if(!setSmoothForAllLayers(g_pState->bSmooth))
    {
        mC64.logMgr().get()->msg(LL_ERROR, "Failed to enable smooth scrolling.\n");
        closeTutorial();
        return -1;
    }

    mC64.logMgr().get()->msg(LL_INFO, "\nCONTROLS:\n [Left/Right cursors] Move\n [Space] Jump\n [S] Toggle smooth scroll\n [P] Pause\n [R] Resume\n [D] Toggle object AABB\n [F1] Toggle trigger zones\n [F2] Toggle tile grid\n [F3] Toggle camera dead zone\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_SPACE) g_pState->bJumpQueued = true;
                if(ev.key.key == SDLK_P) g_pState->pScene->pause();
                if(ev.key.key == SDLK_R) g_pState->pScene->resume();
                if(ev.key.key == SDLK_D)
                {
                    g_pState->bDebugAABB = !g_pState->bDebugAABB;
                    g_pState->pScene->setDebugObjectOverlay(SDO_OBJECT_AABB, SDOT_STANDARD, g_pState->bDebugAABB);
                    mC64.logMgr().get()->msg(LL_INFO, "Debug AABB: %s\n", g_pState->bDebugAABB ? "ON" : "OFF");
                }
                if(ev.key.key == SDLK_F1)
                {
                    g_pState->bDebugTriggers = !g_pState->bDebugTriggers;
                    g_pState->pScene->setDebugObjectOverlay(SDO_TRIGGER_ZONES, SDOT_STANDARD, g_pState->bDebugTriggers);
                    mC64.logMgr().get()->msg(LL_INFO, "Debug Trigger Zones: %s\n", g_pState->bDebugTriggers ? "ON" : "OFF");
                }
                if(ev.key.key == SDLK_F2)
                {
                    g_pState->bDebugTileGrid = !g_pState->bDebugTileGrid;
                    g_pState->pScene->setDebugOverlay(SDO_TILE_CELLGRID, g_pState->bDebugTileGrid);
                    mC64.logMgr().get()->msg(LL_INFO, "Debug Tile Grid: %s\n", g_pState->bDebugTileGrid ? "ON" : "OFF");
                }
                if(ev.key.key == SDLK_F3)
                {
                    g_pState->bDebugCameraDeadZone = !g_pState->bDebugCameraDeadZone;
                    g_pState->pScene->setDebugOverlay(SDO_CAMERA_DEADZONE, g_pState->bDebugCameraDeadZone);
                    mC64.logMgr().get()->msg(LL_INFO, "Debug camera dead zone: %s\n", g_pState->bDebugCameraDeadZone ? "ON" : "OFF");
                }
                if(ev.key.key == SDLK_S)
                {
                    g_pState->bSmooth = !g_pState->bSmooth;
                    if(!setSmoothForAllLayers(g_pState->bSmooth)) mC64.logMgr().get()->msg(LL_ERROR, "Failed to change smooth scrolling.\n");
                    mC64.logMgr().get()->msg(LL_INFO, "Smooth: %s\n", g_pState->bSmooth ? "ON" : "OFF");
                }
            }
        }
        if(bDone) break;

        g_pState->pScene->update();
        if(handleHousePrompt()) bDone = true;
    }

    mC64.sceneMgr().info();
    closeTutorial();
    return 0;
}
