/* ----------------------------------------------------------------------
    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 01: Basic
------------------------------------------------------------------------

    Overview:
    This tutorial opens a window, loads two images, moves a ship with the
    keyboard, and saves a screenshot.

    Key concepts:
    - Start the CRM64Pro memory manager.
    - Access the main engine object and managers.
    - Configure log output, timer and screen.
    - Load PNG images with ImageMgr.
    - Read SDL keyboard and mouse events through Main::update().
    - Update movement at a fixed logic rate.
    - Render images from the main loop.
    - Save the current screen as a PNG file.

    Controls:
    - Cursor Keys: Move the ship image.
    - S: Save a screen snapshot.
    - Any Key/Mouse: Logs events.
    - Q / ESC: Quit.

    Loop order:
    - Process input events.
    - Run one fixed logic update.
    - Render the background and moving ship image.
------------------------------------------------------------------------ */

#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_BG_IMAGE BASEDIR"tutorial_bg.png"
#define RESOURCE_PLAYER_IMAGE BASEDIR"tutorial_ship.png"
#define OUTPUT_SNAPSHOT OUTPUTDIR"Tutorial_01_Basic-Snapshot.png"

// Fixed-rate timing.
// This first tutorial updates and renders once per fixed-rate step.
static const Sint32 iLogicRate = 20;
static const Sint32 iRenderRate = iLogicRate;

// Window size and movement speed.
static const Sint32 iScreenW = 960;
static const Sint32 iScreenH = 540;
static const float fMoveSpeed = 220.0f;

// Values changed while the tutorial runs.
struct TutorialState
{
    Sint32 idBgImage = -1;
    Sint32 idPlayerImage = -1;
    float fPlayerX = 386.0f;
    float fPlayerY = 186.0f;
    bool bMoveLeft = false;
    bool bMoveRight = false;
    bool bMoveUp = false;
    bool bMoveDown = false;
};

// 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;
}

// 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");
}

// Move the ship image one fixed logic step.
static void updateLogic(TutorialState& rState, Image* pPlayerImage, float fDeltaTime)
{
    if(!pPlayerImage) return;

    float fDX = 0.0f;
    float fDY = 0.0f;

    if(rState.bMoveLeft) fDX -= fMoveSpeed * fDeltaTime;
    if(rState.bMoveRight) fDX += fMoveSpeed * fDeltaTime;
    if(rState.bMoveUp) fDY -= fMoveSpeed * fDeltaTime;
    if(rState.bMoveDown) fDY += fMoveSpeed * fDeltaTime;

    const float fMaxX = static_cast<float>(iScreenW - pPlayerImage->getWidth());
    const float fMaxY = static_cast<float>(iScreenH - pPlayerImage->getHeight());
    rState.fPlayerX = clampFloat(rState.fPlayerX + fDX, 0.0f, fMaxX);
    rState.fPlayerY = clampFloat(rState.fPlayerY + fDY, 0.0f, fMaxY);
}

// Draw the current frame.
// This first tutorial renders from main(). The next tutorials move rendering
// into a Screen render callback.
static void renderFrame(Image* pBgImage, Image* pPlayerImage, const TutorialState& rState)
{
    if(pBgImage) pBgImage->render();

    if(pPlayerImage)
    {
        SDL_FRect rDst = {
            rState.fPlayerX,
            rState.fPlayerY,
            static_cast<float>(pPlayerImage->getWidth()),
            static_cast<float>(pPlayerImage->getHeight())
        };
        pPlayerImage->render(0, nullptr, &rDst);
    }
}

// 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_01_Basic", LL_DEBUG, LM_FILE | LM_STDOUT, OUTPUTDIR"Tutorial_01_Basic.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 01: Basic");

    mLog.msg(LL_INFO, "  Create screen (%dx%d) ... ", iScreenW, iScreenH);
    if(pScreen->show() < 0)
    {
        mLog.msg(LL_CRITICAL, "FAILED\n");

        // Standard setup error path:
        // 1. Log what failed.
        // 2. Terminate the engine so initialized managers are cleaned up.
        // 3. Return -1 to report application failure to the operating system.
        Main::terminate();
        return -1;
    }
    logTaskOk(mLog);

    mLog.msg(LL_INFO, "\nRESOURCES: external images\n");

    // Load external PNG images from validation/bin/Base.
    // The second parameter is the resource name stored by ImageMgr.
    mLog.msg(LL_INFO, "  Load background image: %s ... ", RESOURCE_BG_IMAGE);
    state.idBgImage = mC64.imageMgr().loadFromFile(RESOURCE_BG_IMAGE, "tutorialBasicBg");
    if(state.idBgImage < 0)
    {
        logTaskFailed(mLog, state.idBgImage);
        Main::terminate();
        return -1;
    }
    logTaskOk(mLog);

    mLog.msg(LL_INFO, "  Load ship image: %s ... ", RESOURCE_PLAYER_IMAGE);
    state.idPlayerImage = mC64.imageMgr().loadFromFile(RESOURCE_PLAYER_IMAGE, "tutorialBasicPlayer");
    if(state.idPlayerImage < 0)
    {
        logTaskFailed(mLog, state.idPlayerImage);
        Main::terminate();
        return -1;
    }
    logTaskOk(mLog);

    mLog.msg(LL_INFO, "  Access background image object ... ");
    Image* pBgImage = mC64.imageMgr().get(state.idBgImage);
    if(pBgImage == nullptr)
    {
        logTaskFailed(mLog);
        Main::terminate();
        return -1;
    }
    logTaskOk(mLog);

    mLog.msg(LL_INFO, "  Access ship image object ... ");
    Image* pPlayerImage = mC64.imageMgr().get(state.idPlayerImage);
    if(pPlayerImage == nullptr)
    {
        logTaskFailed(mLog);
        Main::terminate();
        return -1;
    }
    logTaskOk(mLog);

    mLog.msg(LL_INFO, "\nCONTROLS\n");
    mLog.msg(LL_INFO, "  [Q] / [ESC] Quit\n");
    mLog.msg(LL_INFO, "  [Arrow Keys] Move the ship image\n");
    mLog.msg(LL_INFO, "  [S] Save a screen snapshot\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() returns 1 while input and render processing continues.
        // It returns 0 when the next fixed logic step 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)
                {
                    state.bMoveLeft = true;
                }
                else if(event.key.key == SDLK_RIGHT)
                {
                    state.bMoveRight = true;
                }
                else if(event.key.key == SDLK_UP)
                {
                    state.bMoveUp = true;
                }
                else if(event.key.key == SDLK_DOWN)
                {
                    state.bMoveDown = true;
                }
                else if(event.key.key == SDLK_S)
                {
                    mLog.msg(LL_INFO, "  Save screen snapshot: %s ... ", OUTPUT_SNAPSHOT);
                    if(pScreen->saveSnapshot(OUTPUT_SNAPSHOT) == 0)
                    {
                        logTaskOk(mLog);
                    }
                    else
                    {
                        logTaskFailed(mLog);
                    }
                }
            }
            else if(event.type == SDL_EVENT_KEY_UP)
            {
                if(event.key.key == SDLK_LEFT) state.bMoveLeft = false;
                else if(event.key.key == SDLK_RIGHT) state.bMoveRight = false;
                else if(event.key.key == SDLK_UP) state.bMoveUp = false;
                else if(event.key.key == SDLK_DOWN) state.bMoveDown = false;
            }
            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;
        updateLogic(state, pPlayerImage, 1.0f / static_cast<float>(iLogicRate));
        renderFrame(pBgImage, pPlayerImage, state);
    }

    mC64.imageMgr().info();
    // Close one image by id, then close the remaining ImageMgr resources.
    // close(0) closes all resources owned by that manager.
    mLog.msg(LL_INFO, "\nCLEANUP\n");
    mLog.msg(LL_INFO, "  Close ship image by id ... ");
    mC64.imageMgr().close(state.idPlayerImage);
    logTaskOk(mLog);
    mLog.msg(LL_INFO, "  Close remaining images ... ");
    mC64.imageMgr().close(0);
    logTaskOk(mLog);
    Main::terminate();

    return 0;
}
