/* ----------------------------------------------------------------------
    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 14: Video
------------------------------------------------------------------------

    Overview:
    This tutorial introduces video playback.
    It loads a video file, assigns a viewport, plays the video with its audio
    stream, and renders a small HUD with playback state and timing.

    Key concepts:
    - Initialize audio before playing videos that include audio streams.
    - Load a video from an external media file with VideoMgr::loadFromFile().
    - Read video metadata with Video::getInfo().
    - Control playback with play(), pause(), resume(), stop() and seek().
    - Set the automatic render target viewport with Video::setViewport().
    - Use the audio tag gain system for video audio volume.

    Controls:
    - Space: Play, pause or resume.
    - S: Stop playback.
    - R: Restart from the beginning.
    - Left / Right: Seek backward or forward.
    - V: Toggle viewport size.
    - + / -: Change video audio gain.
    - D: Toggle debug window.
    - Grave: Toggle console.
    - Q / ESC: Quit.

    Loop order:
    - Process input events.
    - Run one fixed logic update.
    - Let Main::update() call the screen render callback.
    - Let VideoMgr render the active video after the screen 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_VIDEO_FILE BASEDIR"Video/h264-360p-30fps-acc.mov"
#define RESOURCE_BG_CDC_NAME "Tutorial_bg"
#define RESOURCE_CURSOR_CDC_NAME "Tutorial_cursor"
#define OUTPUT_CDC OUTPUTDIR"Tutorial.cdc"

// Render rate 0 lets the screen render as often as possible.
// Logic rate controls video and GUI updates.
static const Sint32 iLogicRate = 20;
static const Sint32 iRenderRate = 0;

// Window size.
static const Sint32 iScreenW = 960;
static const Sint32 iScreenH = 540;

// Tutorial playback settings.
static const Sint32 iSeekStepMS = 5000;
static const float fGainStep = 0.10f;

// Values changed while the tutorial runs.
struct TutorialState
{
    Sint32 idBgImage = -1;
    Sint32 idCursor = -1;
    Sint32 idVideo = -1;
    Sint32 idBuiltInFont = -1;
    Sint32 idDebugWindow = -1;
    VideoInfo VI = {};
    bool bLargeViewport = false;
    float fMouseX = 0.0f;
    float fMouseY = 0.0f;
    float fVideoGain = 1.0f;
    Sint32 iPlaybackPosition = 0;
    Sint32 iPlaybackDuration = 0;
    Sint32 iCurrentRenderRate = 0;
    Sint32 iCurrentLogicRate = 0;
    Sint32 iLogicFrames = 0;
    char szLastAction[128] = "Ready";
};

// 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");
}

// Store the action shown by the tutorial.
static void setLastAction(TutorialState& rState, const char* szAction)
{
    snprintf(rState.szLastAction, sizeof(rState.szLastAction), "%s", szAction);
}

// Return the text for a playback status.
static const char* playbackStatusName(ePlaybackStatus eStatus)
{
    if(eStatus == PS_PLAYING) return "Playing";
    if(eStatus == PS_PAUSED) return "Paused";
    if(eStatus == PS_FINISHED) return "Finished";
    return "Stopped";
}

// Update the tutorial state for the current frame.
static void updateLogic(TutorialState& rState)
{
    rState.iLogicFrames++;
}

// Draw text at the requested screen position.
static void renderText(Font* pFont, Sint32 iX, Sint32 iY, const char* szText)
{
    if(!pFont) return;
    pFont->setPosition(iX, iY);
    pFont->render(szText);
}

// Read the current video playback state.
static void refreshVideoState(Video* pVideo, TutorialState& rState)
{
    if(!pVideo)
    {
        rState.iPlaybackPosition = 0;
        rState.iPlaybackDuration = 0;
        return;
    }

    rState.iPlaybackPosition = pVideo->getPlaybackPosition();
    rState.iPlaybackDuration = pVideo->getDuration();
}

// Refresh the video state used by the tutorial.
static void refreshRuntimeState(Main& mC64, Video* pVideo, TutorialState& rState)
{
    refreshVideoState(pVideo, rState);
    rState.fMouseX = mC64.cursorMgr().getX();
    rState.fMouseY = mC64.cursorMgr().getY();
    rState.iCurrentRenderRate = static_cast<Sint32>(mC64.timer().getCurrentRFR());
    rState.iCurrentLogicRate = static_cast<Sint32>(mC64.timer().getCurrentLFR());
}

// The video is rendered by VideoMgr after this callback.
// Keep the HUD outside the viewport so the video does not cover the text.
static void renderHUD(Font* pBuiltInFont, const TutorialState& rState, ePlaybackStatus eStatus)
{
    if(!pBuiltInFont) return;

    char szLine[180];
    snprintf(szLine, sizeof(szLine), "Tutorial 14: Video   Render: %d fps   Logic: %d fps", rState.iCurrentRenderRate, rState.iCurrentLogicRate);
    renderText(pBuiltInFont, 10, 10, szLine);

    snprintf(szLine, sizeof(szLine), "File: %s", RESOURCE_VIDEO_FILE);
    renderText(pBuiltInFont, 10, 40, szLine);

    snprintf(szLine, sizeof(szLine), "Status: %s   Position: %d / %d ms   Gain: %.1f",
        playbackStatusName(eStatus), rState.iPlaybackPosition, rState.iPlaybackDuration, rState.fVideoGain);
    renderText(pBuiltInFont, 10, 70, szLine);

    snprintf(szLine, sizeof(szLine), "Video: %dx%d   Codec: %s   Audio: %s",
        rState.VI.iWidth, rState.VI.iHeight, rState.VI.szCodec, rState.VI.szAudioCodec);
    renderText(pBuiltInFont, 10, 100, szLine);

    renderText(pBuiltInFont, 10, 455, "Space play/pause   S stop   R restart   Left/Right seek   V viewport   +/- gain   D debug");
    snprintf(szLine, sizeof(szLine), "Last action: %s", rState.szLastAction);
    renderText(pBuiltInFont, 10, 485, szLine);
}

// Draw the current frame. CursorMgr draws the selected cursor automatically.
static void renderFrame(Image* pBgImage, Font* pBuiltInFont, Video* pVideo, const TutorialState& rState)
{
    if(pBgImage) pBgImage->render();
    renderHUD(pBuiltInFont, rState, pVideo ? pVideo->status() : PS_STOPPED);
}

// Screen render callback.
// Main::update() calls this before the automatic video and GUI rendering passes.
// Runtime values are cached from the logic loop; render only draws that state.
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);
    Font* pBuiltInFont = mC64.fontMgr().get(pState->idBuiltInFont);
    Video* pVideo = mC64.videoMgr().get(pState->idVideo);
    renderFrame(pBgImage, pBuiltInFont, pVideo, *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();
}

// Keep the audio gain within its valid range.
static float clampGain(float fGain)
{
    if(fGain < 0.0f) return 0.0f;
    if(fGain > 2.0f) return 2.0f;
    return fGain;
}

// Keep the video aspect ratio while changing only the destination area.
static bool applyVideoViewport(Screen* pScreen, Video* pVideo, bool bLargeViewport)
{
    if(!pScreen || !pVideo) return false;

    const float fScreenW = static_cast<float>(pScreen->getWidth());
    const float fAreaX = bLargeViewport ? 30.0f : 120.0f;
    const float fAreaY = bLargeViewport ? 130.0f : 155.0f;
    const float fAreaW = fScreenW - (fAreaX * 2.0f);
    const float fAreaH = bLargeViewport ? 295.0f : 245.0f;
    const float fVideoW = static_cast<float>(pVideo->getWidth());
    const float fVideoH = static_cast<float>(pVideo->getHeight());

    if(fVideoW <= 0.0f || fVideoH <= 0.0f) return false;

    const float fScale = SDL_min(fAreaW / fVideoW, fAreaH / fVideoH);
    SDL_FRect rDst;
    rDst.w = fVideoW * fScale;
    rDst.h = fVideoH * fScale;
    rDst.x = fAreaX + ((fAreaW - rDst.w) * 0.5f);
    rDst.y = fAreaY + ((fAreaH - rDst.h) * 0.5f);

    return pVideo->setTarget() && pVideo->setViewport(&rDst);
}

// Start, pause, or resume the current video.
static void playPauseOrResume(Video* pVideo, TutorialState& rState)
{
    if(!pVideo) return;

    if(pVideo->status() == PS_PLAYING)
    {
        setLastAction(rState, pVideo->pause() ? "Paused video" : "Pause failed");
    }
    else if(pVideo->status() == PS_PAUSED)
    {
        setLastAction(rState, pVideo->resume() ? "Resumed video" : "Resume failed");
    }
    else
    {
        setLastAction(rState, pVideo->play(0) >= 0 ? "Started video" : "Play failed");
    }
}

// Move the video position by the requested time.
static void seekRelative(Video* pVideo, TutorialState& rState, Sint32 iDeltaMS)
{
    if(!pVideo) return;

    const Sint32 iDuration = pVideo->getDuration();
    Sint32 iTarget = pVideo->getPlaybackPosition() + iDeltaMS;
    if(iTarget < 0) iTarget = 0;
    if(iDuration > 0 && iTarget > iDuration) iTarget = iDuration;

    if(!pVideo->seek(iTarget))
    {
        setLastAction(rState, "Seek failed");
        return;
    }

    char szAction[128];
    snprintf(szAction, sizeof(szAction), "Seek to %d ms", iTarget);
    setLastAction(rState, szAction);
}

// 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_14_Video", LL_DEBUG, LM_FILE | LM_STDOUT, OUTPUTDIR"Tutorial_14_Video.log");

    mLog.msg(LL_INFO, "\nSETUP: screen, timer and audio\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 14: Video");

    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, "  Initialize audio ... ");
    if(!mC64.configMgr().audioInit())
    {
        logTaskFailed(mLog);
        Main::terminate();
        return -1;
    }
    logTaskOk(mLog);

    // Video audio uses the music tag in this tutorial.
    mC64.configMgr().audioSetMasterGain(1.0f);
    mC64.configMgr().audioSetTagGain(ATT_MUSIC, state.fVideoGain);

    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: background, cursor, font and video\n");

    // Earlier tutorials store the reusable graphics resources in Tutorial.cdc.
    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 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 content font ... ");
    state.idBuiltInFont = mC64.fontMgr().getBuiltin("CourierNew10White");
    if(state.idBuiltInFont < 0 || mC64.fontMgr().get(state.idBuiltInFont) == nullptr)
    {
        logTaskFailed(mLog, state.idBuiltInFont);
        Main::terminate();
        return -1;
    }
    logTaskOk(mLog);

    // VideoMgr owns the loaded media object. The returned Video pointer is borrowed.
    mLog.msg(LL_INFO, "  Load video file: %s ... ", RESOURCE_VIDEO_FILE);
    state.idVideo = mC64.videoMgr().loadFromFile(RESOURCE_VIDEO_FILE, "TutorialVideo", ATT_MUSIC);
    Video* pVideo = mC64.videoMgr().get(state.idVideo);
    if(state.idVideo < 0 || pVideo == nullptr)
    {
        logTaskFailed(mLog, state.idVideo);
        Main::terminate();
        return -1;
    }
    logTaskOk(mLog);

    // Metadata is useful for diagnostics and for deciding how to place the video.
    mLog.msg(LL_INFO, "  Read video metadata ... ");
    if(!pVideo->getInfo(&state.VI))
    {
        logTaskFailed(mLog);
        Main::terminate();
        return -1;
    }
    logTaskOk(mLog);

    mLog.msg(LL_INFO, "  Set video viewport ... ");
    if(!applyVideoViewport(pScreen, pVideo, state.bLargeViewport))
    {
        logTaskFailed(mLog);
        Main::terminate();
        return -1;
    }
    logTaskOk(mLog);

    mLog.msg(LL_INFO, "\nTOOLS: debug window and console\n");

    // The DebugWindow shows playback values while the HUD remains minimal.
    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("Mouse X", nullptr, &state.fMouseX);
    pDebug->addWatch("Mouse Y", nullptr, &state.fMouseY);
    pDebug->addWatch("Position", &state.iPlaybackPosition);
    pDebug->addWatch("Duration", &state.iPlaybackDuration);
    pDebug->addWatch("Audio gain", nullptr, &state.fVideoGain);
    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 14: Video ready. Press ` to toggle the console.\n");

    mLog.msg(LL_INFO, "\nPLAYBACK\n");
    mLog.msg(LL_INFO, "  Start video ... ");
    if(pVideo->play(0) < 0)
    {
        logTaskFailed(mLog);
        Main::terminate();
        return -1;
    }
    setLastAction(state, "Started video");
    logTaskOk(mLog);

    mLog.msg(LL_INFO, "\nCONTROLS\n");
    mLog.msg(LL_INFO, "  [Space] Play / pause / resume\n");
    mLog.msg(LL_INFO, "  [S] Stop   [R] Restart   [Left]/[Right] Seek\n");
    mLog.msg(LL_INFO, "  [V] Toggle viewport   [-]/[=] Change audio gain\n");
    mLog.msg(LL_INFO, "  [D] Toggle debug window   [`] Toggle console\n");
    mLog.msg(LL_INFO, "  [Q] / [ESC] Quit\n");
    mLog.msg(LL_INFO, "\nRUNNING\n");

    SDL_Event event;
    bool bRunning = true;
    while(bRunning)
    {
        // Main::update() processes input, runs internal GUI logic, advances video
        // playback 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)
            {
                if(event.key.key == SDLK_Q || event.key.key == SDLK_ESCAPE)
                {
                    bRunning = false;
                }
                else if(event.key.key == SDLK_SPACE)
                {
                    playPauseOrResume(pVideo, state);
                }
                else if(event.key.key == SDLK_S)
                {
                    pVideo->stop();
                    setLastAction(state, "Stopped video");
                }
                else if(event.key.key == SDLK_R)
                {
                    pVideo->stop();
                    setLastAction(state, pVideo->play(0) >= 0 ? "Restarted video" : "Restart failed");
                }
                else if(event.key.key == SDLK_LEFT)
                {
                    seekRelative(pVideo, state, -iSeekStepMS);
                }
                else if(event.key.key == SDLK_RIGHT)
                {
                    seekRelative(pVideo, state, iSeekStepMS);
                }
                else if(event.key.key == SDLK_V)
                {
                    const bool bLargeViewport = !state.bLargeViewport;
                    if(applyVideoViewport(pScreen, pVideo, bLargeViewport))
                    {
                        state.bLargeViewport = bLargeViewport;
                        setLastAction(state, state.bLargeViewport ? "Large viewport" : "Normal viewport");
                    }
                    else setLastAction(state, "Viewport change failed");
                }
                else if(event.key.key == SDLK_MINUS)
                {
                    state.fVideoGain = clampGain(state.fVideoGain - fGainStep);
                    mC64.configMgr().audioSetTagGain(ATT_MUSIC, state.fVideoGain);
                    setLastAction(state, "Reduced video audio gain");
                }
                else if(event.key.key == SDLK_EQUALS)
                {
                    state.fVideoGain = clampGain(state.fVideoGain + fGainStep);
                    mC64.configMgr().audioSetTagGain(ATT_MUSIC, state.fVideoGain);
                    setLastAction(state, "Increased video audio gain");
                }
                else if(event.key.key == SDLK_D)
                {
                    toggleDebugWindow(pDebug);
                }
            }
        }

        if(!bRunning) break;
        refreshRuntimeState(mC64, pVideo, state);
        updateLogic(state);
    }

    // Close resources owned by this tutorial.
    mC64.videoMgr().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 videos ... ");
    mC64.videoMgr().close(0);
    logTaskOk(mLog);
    mLog.msg(LL_INFO, "  Close audio ... ");
    mC64.configMgr().audioClose();
    logTaskOk(mLog);
    mLog.msg(LL_INFO, "  Close fonts ... ");
    mC64.fontMgr().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;
}
