/* ----------------------------------------------------------------------
    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 07: Tileset
------------------------------------------------------------------------

    Overview:
    This tutorial extends Tutorial 06 GFX with a tileset resource.
    It adds tiles.bmp to Tutorial.cdc, loads the tileset from the CDC, renders
    a simple map, and moves the ship from tile to tile.

    Key concepts:
    - Create a Tile from an Image.
    - Set the image colorkey before saving the Tile to the CDC.
    - Split a tileset image into 48x48 tiles.
    - Render a map by drawing tile IDs.
    - Allow movement only through walkable tile IDs.

    Controls:
    - Cursor Keys: Move the ship to the next walkable tile.
    - D: Toggle debug window.
    - Grave: Toggle console.
    - Any Key/Mouse: Logs events.
    - Q / ESC: Quit.

    Loop order:
    - Process input events.
    - Run one fixed logic update.
    - Let Main::update() call the screen render callback.
------------------------------------------------------------------------ */

#include "CRM64Pro.h"
#include "RunParameter.h"

using namespace CRM64Pro;

#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_TILE_IMAGE BASEDIR"tiles.bmp"
#define RESOURCE_BG_CDC_NAME "Tutorial_bg"
#define RESOURCE_PLAYER_CDC_NAME "Tutorial_ship"
#define RESOURCE_CURSOR_CDC_NAME "Tutorial_cursor"
#define RESOURCE_TILE_CDC_NAME "Tutorial_tiles"
#define OUTPUT_CDC OUTPUTDIR"Tutorial.cdc"

// Fixed-rate timing.
// Render rate 0 lets the screen render as often as possible. Logic rate controls input and counters.
static const Sint32 iLogicRate = 20;
static const Sint32 iRenderRate = 0;

// Window and tileset layout.
static const Sint32 iScreenW = 960;
static const Sint32 iScreenH = 540;
static const Sint32 iTileSize = 48;
static const Sint32 iMapW = 12;
static const Sint32 iMapH = 8;
static const Sint32 iMapX = (iScreenW - (iMapW * iTileSize)) / 2;
static const Sint32 iMapY = (iScreenH - (iMapH * iTileSize)) / 2;

// Tile IDs inside tiles.bmp.
static const Uint8 iTileStars = 1;
static const Uint8 iTileMoon = 2;
static const Uint8 iTileRedBlock = 3;
static const Uint8 iTileBlueBlock = 4;

// 1 and 2 are walkable. 3 and 4 block movement.
static const Uint8 iMap[iMapH][iMapW] = {
    { 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3 },
    { 3, 1, 1, 1, 2, 1, 1, 4, 1, 1, 2, 3 },
    { 3, 1, 4, 1, 3, 3, 1, 4, 1, 4, 1, 3 },
    { 3, 2, 4, 1, 1, 1, 1, 1, 1, 4, 1, 3 },
    { 3, 1, 4, 4, 4, 2, 4, 4, 1, 1, 1, 3 },
    { 3, 1, 1, 1, 1, 1, 4, 2, 1, 4, 1, 3 },
    { 3, 2, 4, 1, 4, 1, 1, 1, 1, 4, 2, 3 },
    { 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3 }
};

// Values changed while the tutorial runs.
struct TutorialState
{
    Sint32 idBgImage = -1;
    Sint32 idPlayerImage = -1;
    Sint32 idCursor = -1;
    Sint32 idTileSet = -1;
    Sint32 idBuiltInFont = -1;
    Sint32 idDebugWindow = -1;
    Sint32 iPlayerCellX = 1;
    Sint32 iPlayerCellY = 1;
    float fPlayerX = 0.0f;
    float fPlayerY = 0.0f;
    float fMouseX = 0.0f;
    float fMouseY = 0.0f;
    Sint32 iCurrentRenderRate = 0;
    Sint32 iCurrentLogicRate = 0;
    Sint32 iLogicFrames = 0;
};

// Helper functions used by the setup log messages.
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");
}

// Check whether a tile can be walked on.
static bool isWalkableTile(Uint8 iTile)
{
    return iTile == iTileStars || iTile == iTileMoon;
}

// Check whether the map cell can be walked on.
static bool isWalkableCell(Sint32 iCellX, Sint32 iCellY)
{
    if(iCellX < 0 || iCellX >= iMapW) return false;
    if(iCellY < 0 || iCellY >= iMapH) return false;
    return isWalkableTile(iMap[iCellY][iCellX]);
}

// Place the player at the requested map cell.
static void placePlayerOnCell(TutorialState& rState, const Image* pPlayerImage)
{
    if(!pPlayerImage) return;

    const float fCellCenterX = static_cast<float>(iMapX + rState.iPlayerCellX * iTileSize + iTileSize / 2);
    const float fCellCenterY = static_cast<float>(iMapY + rState.iPlayerCellY * iTileSize + iTileSize / 2);
    rState.fPlayerX = fCellCenterX - static_cast<float>(pPlayerImage->getWidth()) * 0.5f;
    rState.fPlayerY = fCellCenterY - static_cast<float>(pPlayerImage->getHeight()) * 0.5f;
}

// Move the player when the destination cell is walkable.
static void tryMovePlayer(TutorialState& rState, Image* pPlayerImage, Sint32 iDX, Sint32 iDY, Log& mLog)
{
    const Sint32 iNextX = rState.iPlayerCellX + iDX;
    const Sint32 iNextY = rState.iPlayerCellY + iDY;

    if(!isWalkableCell(iNextX, iNextY))
    {
        mLog.msg(LL_INFO, "  Movement blocked at cell %d, %d\n", iNextX, iNextY);
        return;
    }

    rState.iPlayerCellX = iNextX;
    rState.iPlayerCellY = iNextY;
    placePlayerOnCell(rState, pPlayerImage);
    mLog.msg(LL_INFO, "  Ship moved to cell %d, %d\n", rState.iPlayerCellX, rState.iPlayerCellY);
}

// Update the tutorial state for the current frame.
static void updateLogic(TutorialState& rState)
{
    rState.iLogicFrames++;
}

// Draw the tile map.
static void renderMap(Tile* pTileSet)
{
    if(!pTileSet) return;

    for(Sint32 iY = 0; iY < iMapH; ++iY)
    {
        for(Sint32 iX = 0; iX < iMapW; ++iX)
        {
            pTileSet->setPosition(static_cast<float>(iMapX + iX * iTileSize), static_cast<float>(iMapY + iY * iTileSize));
            if(iMap[iY][iX] == iTileMoon)
            {
                pTileSet->render(iTileStars);
            }
            pTileSet->render(iMap[iY][iX]);
        }
    }
}

// Draw the current timing information.
static void renderRates(Font* pBuiltInFont, const TutorialState& rState)
{
    if(!pBuiltInFont) return;

    char szRates[96];
    snprintf(szRates, sizeof(szRates), "Render: %d fps   Logic: %d fps", rState.iCurrentRenderRate, rState.iCurrentLogicRate);
    pBuiltInFont->setPosition(10, 10);
    pBuiltInFont->render(szRates);
}

// Draw the current frame. CursorMgr draws the selected cursor automatically.
static void renderFrame(Image* pBgImage, Tile* pTileSet, Image* pPlayerImage, Font* pBuiltInFont, const TutorialState& rState)
{
    if(pBgImage) pBgImage->render();

    renderMap(pTileSet);

    if(pPlayerImage)
    {
        SDL_FRect rDst = {
            rState.fPlayerX,
            rState.fPlayerY,
            static_cast<float>(pPlayerImage->getWidth()),
            static_cast<float>(pPlayerImage->getHeight())
        };
        pPlayerImage->render(0, nullptr, &rDst);
    }

    renderRates(pBuiltInFont, rState);
}

// Screen render callback.
// Main::update() calls this when the screen is ready to render.
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);
    Tile* pTileSet = mC64.tileMgr().get(pState->idTileSet);
    Image* pPlayerImage = mC64.imageMgr().get(pState->idPlayerImage);
    Font* pBuiltInFont = mC64.fontMgr().get(pState->idBuiltInFont);
    renderFrame(pBgImage, pTileSet, pPlayerImage, pBuiltInFont, *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();
}

// Add the tileset resource to the tutorial archive.
static bool addTilesetToCDC(Main& mC64, Log& mLog)
{
    // Tutorial 07 introduces one new resource. Earlier tutorials already added
    // the background, ship and cursor to the CDC.
    mLog.msg(LL_INFO, "  Find Tutorial 04 CDC: %s ... ", OUTPUT_CDC);
    if(!mC64.tool().fileExists(OUTPUT_CDC))
    {
        logTaskFailed(mLog);
        return false;
    }
    logTaskOk(mLog);

    mLog.msg(LL_INFO, "  Load tileset image: %s ... ", RESOURCE_TILE_IMAGE);
    Sint32 idTileImage = mC64.imageMgr().loadFromFile(RESOURCE_TILE_IMAGE, "tutorialTileImageFile");
    Image* pTileImage = mC64.imageMgr().get(idTileImage);
    if(idTileImage < 0 || pTileImage == nullptr)
    {
        logTaskFailed(mLog, idTileImage);
        return false;
    }
    pTileImage->setColorKey(255, 0, 255);
    logTaskOk(mLog);

    mLog.msg(LL_INFO, "  Create tile from image ... ");
    Sint32 idTileFile = mC64.tileMgr().create(RESOURCE_TILE_CDC_NAME);
    Tile* pTileFile = mC64.tileMgr().get(idTileFile);
    if(idTileFile < 0 || pTileFile == nullptr)
    {
        logTaskFailed(mLog, idTileFile);
        mC64.imageMgr().close(idTileImage);
        return false;
    }
    if(pTileFile->assignImage(idTileImage, 1) < 0)
    {
        logTaskFailed(mLog);
        mC64.tileMgr().close(idTileFile);
        mC64.imageMgr().close(idTileImage);
        return false;
    }
    if(!pTileFile->setOffset(1, iTileSize, iTileSize))
    {
        logTaskFailed(mLog);
        mC64.tileMgr().close(idTileFile);
        return false;
    }
    logTaskOk(mLog);

    // Save appends or replaces the tileset resource in Tutorial.cdc.
    mLog.msg(LL_INFO, "  Save tileset to CDC ... ");
    if(pTileFile->save(OUTPUT_CDC, RESOURCE_TILE_CDC_NAME) < 0)
    {
        logTaskFailed(mLog);
        mC64.tileMgr().close(idTileFile);
        return false;
    }
    logTaskOk(mLog);

    // Close the temporary tile. The tutorial loads a fresh copy from CDC next.
    mLog.msg(LL_INFO, "  Close temporary tile ... ");
    mC64.tileMgr().close(idTileFile);
    logTaskOk(mLog);

    return true;
}

// 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;

    // Start CRM64Pro memory tracking before creating engine resources.
    // atexit() releases memory tracking data when the program exits.
    CMem::setStatsLevel(CMem::MSL_NORMAL);
    atexit(CMem::destroy);

    // Main gives access to the CRM64Pro managers.
    Main& mC64 = Main::instance();
    Log& mLog = *mC64.logMgr().get();

    TutorialState state;

    // Write log output to stdout and to a file under validation/bin.
    mLog.init("Tutorial_07_Tileset", LL_DEBUG, LM_FILE | LM_STDOUT, OUTPUTDIR"Tutorial_07_Tileset.log");

    mLog.msg(LL_INFO, "\nSETUP: screen and timing\n");

    // Initialize the timer before calling Main::update().
    // setRate() receives render frames per second, then logic frames per second.
    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);

    // Add a 1 ms idle delay inside Main::update().
    mC64.configMgr().setMTFriendly(1);

    // Configure and show the default screen.
    Screen* pScreen = mC64.configMgr().get();
    pScreen->setSize(iScreenW, iScreenH);
    pScreen->setDriver(eRenderer);
    pScreen->setTitle("CRM64Pro - Tutorial 07: Tileset");

    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: add tileset and load from CDC\n");

    if(!addTilesetToCDC(mC64, mLog))
    {
        Main::terminate();
        return -1;
    }

    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 ship image from CDC ... ");
    state.idPlayerImage = mC64.imageMgr().load(OUTPUT_CDC, RESOURCE_PLAYER_CDC_NAME);
    if(state.idPlayerImage < 0)
    {
        logTaskFailed(mLog, state.idPlayerImage);
        Main::terminate();
        return -1;
    }
    logTaskOk(mLog);

    Image* pPlayerImage = mC64.imageMgr().get(state.idPlayerImage);
    if(pPlayerImage == nullptr)
    {
        logTaskFailed(mLog);
        Main::terminate();
        return -1;
    }
    placePlayerOnCell(state, pPlayerImage);

    mLog.msg(LL_INFO, "  Load tileset from CDC ... ");
    state.idTileSet = mC64.tileMgr().load(OUTPUT_CDC, RESOURCE_TILE_CDC_NAME);
    Tile* pTileSet = mC64.tileMgr().get(state.idTileSet);
    if(state.idTileSet < 0 || pTileSet == nullptr)
    {
        logTaskFailed(mLog, state.idTileSet);
        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");
    Font* pBuiltInFont = mC64.fontMgr().get(state.idBuiltInFont);
    if(state.idBuiltInFont < 0 || pBuiltInFont == nullptr)
    {
        logTaskFailed(mLog, state.idBuiltInFont);
        Main::terminate();
        return -1;
    }
    logTaskOk(mLog);

    mLog.msg(LL_INFO, "\nTOOLS: debug window and console\n");

    // The DebugWindow is available, but this tutorial starts with it hidden.
    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("Player cell X", &state.iPlayerCellX);
    pDebug->addWatch("Player cell Y", &state.iPlayerCellY);
    pDebug->addWatch("Player X", nullptr, &state.fPlayerX);
    pDebug->addWatch("Player Y", nullptr, &state.fPlayerY);
    pDebug->addWatch("Mouse X", nullptr, &state.fMouseX);
    pDebug->addWatch("Mouse Y", nullptr, &state.fMouseY);
    pDebug->addWatch("Render fps", &state.iCurrentRenderRate);
    pDebug->addWatch("Logic fps", &state.iCurrentLogicRate);
    pDebug->addWatch("Logic frames", &state.iLogicFrames);
    pDebug->baseWidget().hide();
    logTaskOk(mLog);

    // Main creates the default Console. Log output is also sent there.
    Console* pConsole = mC64.guiMgr().getConsole();
    if(pConsole) pConsole->print("Tutorial 07: Tileset 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, "  [Arrow Keys] Move the ship to the next walkable tile\n");
    mLog.msg(LL_INFO, "  [D] Toggle debug window\n");
    mLog.msg(LL_INFO, "  [`] Toggle console\n");
    mLog.msg(LL_INFO, "  Any key or mouse button logs the input event\n");
    mLog.msg(LL_INFO, "\nRUNNING\n");

    SDL_Event event;
    bool bRunning = true;
    while(bRunning)
    {
        // Main::update() processes input, runs internal GUI logic and calls the
        // screen render callback when a render frame is ready.
        while(mC64.update(&event))
        {
            if(event.type == SDL_EVENT_QUIT)
            {
                bRunning = false;
            }
            else if(event.type == SDL_EVENT_KEY_DOWN)
            {
                // SDL3 stores the key value in event.key.key.
                mLog.msg(LL_INFO, "  Key pressed: %s (%d)\n", SDL_GetKeyName(event.key.key), event.key.key);

                if(event.key.key == SDLK_Q || event.key.key == SDLK_ESCAPE)
                {
                    bRunning = false;
                }
                else if(event.key.key == SDLK_LEFT)
                {
                    tryMovePlayer(state, pPlayerImage, -1, 0, mLog);
                }
                else if(event.key.key == SDLK_RIGHT)
                {
                    tryMovePlayer(state, pPlayerImage, 1, 0, mLog);
                }
                else if(event.key.key == SDLK_UP)
                {
                    tryMovePlayer(state, pPlayerImage, 0, -1, mLog);
                }
                else if(event.key.key == SDLK_DOWN)
                {
                    tryMovePlayer(state, pPlayerImage, 0, 1, mLog);
                }
                else if(event.key.key == SDLK_D)
                {
                    toggleDebugWindow(pDebug);
                }
            }
            else if(event.type == SDL_EVENT_MOUSE_BUTTON_DOWN)
            {
                mLog.msg(LL_INFO, "  Mouse clicked: button %d at %d, %d\n", event.button.button, (Sint32)event.button.x, (Sint32)event.button.y);
            }
        }

        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);
    }

    // Close resources owned by this tutorial.
    mC64.tileMgr().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 tiles ... ");
    mC64.tileMgr().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;
}
