// Copyright (c) 2012-2024 Wojciech Figat. All rights reserved.

#include "../Game.h"

#if !USE_EDITOR

#include "Engine/Engine/Time.h"
#include "Engine/Engine/Engine.h"
#include "Engine/Engine/Globals.h"
#include "Engine/Platform/Window.h"
#include "Engine/Profiler/Profiler.h"
#include "Engine/Level/Level.h"
#include "Engine/Core/Config/GameSettings.h"
#include "Engine/Content/Content.h"
#include "Engine/Content/Assets/Texture.h"
#include "Engine/Content/JsonAsset.h"
#include "Engine/Content/AssetReference.h"
#include "Engine/Graphics/RenderTask.h"
#include "Engine/Render2D/Render2D.h"
#include "Engine/Graphics/GPUDevice.h"
#include "Engine/Graphics/Textures/GPUTexture.h"
#include "Engine/Utilities/Encryption.h"
#include "Engine/Core/Log.h"
#include "FlaxEngine.Gen.h"

namespace GameBaseImpl
{
    GameHeaderFlags HeaderFlags = GameHeaderFlags::None;
    Guid SplashScreenId;
    float SplashScreenTime = 0.0f;
    AssetReference<JsonAsset> FirstScene;
    AssetReference<Texture> SplashScreen;

    void OnMainWindowClosed();
    void OnPostRender(GPUContext* context, RenderContext& renderContext);
    void OnSplashScreenEnd();
}

bool GameBase::IsShowingSplashScreen()
{
    return GameBaseImpl::SplashScreenTime > ZeroTolerance;
}

void GameBase::InitMainWindowSettings(CreateWindowSettings& settings)
{
}

int32 GameBase::LoadProduct()
{
    // Startup and project root paths are the same in build game
    Globals::ProjectFolder = Globals::StartupFolder;

    // Load build game header file
    {
        int32 tmp;
        Array<byte> data;
        FileReadStream* stream = nullptr;

#if 1
        // Open file
        stream = FileReadStream::Open(Globals::ProjectFolder / TEXT("Content/head"));
        if (stream == nullptr)
            goto LOAD_GAME_HEAD_FAILED;

        // Check header
        stream->ReadInt32(&tmp);
        if (tmp != ('x' + 'D') * 131)
            goto LOAD_GAME_HEAD_FAILED;

        // Don't allow to load game packaged by the other engine version
        stream->ReadInt32(&tmp);
        if (tmp != FLAXENGINE_VERSION_BUILD)
            goto LOAD_GAME_HEAD_FAILED;

        // Load game primary data
        stream->ReadArray(&data);
        if (data.Count() != 808 + sizeof(Guid))
            goto LOAD_GAME_HEAD_FAILED;
        Encryption::DecryptBytes(data.Get(), data.Count());
        Globals::ProductName = (const Char*)data.Get();
        Globals::CompanyName = (const Char*)(data.Get() + 400);
        GameBaseImpl::HeaderFlags = (GameHeaderFlags)*(int32*)(data.Get() + 800);
        Globals::ContentKey = *(int32*)(data.Get() + 804);
        GameBaseImpl::SplashScreenId = *(Guid*)(data.Get() + 808);

        goto LOAD_GAME_HEAD_DONE;

    LOAD_GAME_HEAD_FAILED:
        Platform::Fatal(TEXT("Cannot load game."));
        return -3;

    LOAD_GAME_HEAD_DONE:
        Delete(stream);
#else
        // Mock the settings to debug executable without content
        Globals::ProductName = TEXT("Flax");
        Globals::CompanyName = TEXT("Flax");
        GameBaseImpl::HeaderFlags = GameHeaderFlags::ShowSplashScreen;
        GameBaseImpl::SplashScreenId = Guid::Empty;
        Globals::ContentKey = 0;
#endif
    }

    return 0;
}

bool GameBase::Init()
{
    PROFILE_CPU();

    // Preload splash screen texture
    if (EnumHasAnyFlags(GameBaseImpl::HeaderFlags, GameHeaderFlags::ShowSplashScreen))
    {
        LOG(Info, "Loading splash screen");
        if (GameBaseImpl::SplashScreenId)
            GameBaseImpl::SplashScreen = Content::LoadAsync<Texture>(GameBaseImpl::SplashScreenId);
        else
            GameBaseImpl::SplashScreen = Content::LoadAsyncInternal<Texture>(TEXT("Engine/Textures/Logo"));
        if (!GameBaseImpl::SplashScreen)
        {
            LOG(Error, "Missing splash screen texture!");
        }
    }

    // Preload first scene asset data
    const auto gameSettings = GameSettings::Get();
    if (!gameSettings)
        return true;
    GameBaseImpl::FirstScene = gameSettings->FirstScene;

    return false;
}

void GameBase::BeforeRun()
{
    // Headless more case (no window)
    if (Engine::IsHeadless())
    {
        GameBaseImpl::OnSplashScreenEnd();
        return;
    }

    // Show game window
    LOG(Info, "Showing game window");
    ASSERT(Engine::MainWindow);
    Engine::MainWindow->Show();

    // Show splash screen if it should be used
    if (GameBaseImpl::SplashScreen && GPUDevice::Instance && GPUDevice::Instance->GetRendererType() != RendererType::Null)
    {
        LOG(Info, "Showing splash screen");
        if (MainRenderTask::Instance == nullptr)
            Platform::Fatal(TEXT("Missing main rendering task object."));
        GameBaseImpl::SplashScreenTime = 0;
        MainRenderTask::Instance->PostRender.Bind(&GameBaseImpl::OnPostRender);
    }
    else
    {
        GameBaseImpl::OnSplashScreenEnd();
    }
}

void GameBase::BeforeExit()
{
}

Window* GameBase::CreateMainWindow()
{
    CreateWindowSettings settings;
    settings.Title = *Globals::ProductName;
    settings.AllowDragAndDrop = false;
    settings.Fullscreen = true;
    settings.HasSizingFrame = false;
    settings.HasBorder = false;
    settings.AllowMaximize = true;
    settings.AllowMinimize = true;
    settings.Size = Platform::GetDesktopSize();
    settings.Position = Float2::Zero;

    Game::InitMainWindowSettings(settings);

    const auto window = Platform::CreateWindow(settings);
    if (window)
    {
        window->Closed.Bind(&GameBaseImpl::OnMainWindowClosed);
    }

    return window;
}

void GameBaseImpl::OnMainWindowClosed()
{
    const auto window = Engine::MainWindow;
    if (!window)
        return;

    // Clear field (window is deleting itself)
    Engine::MainWindow = nullptr;

    // Request engine exit
    Globals::IsRequestingExit = true;
    Engine::RequestingExit();
}

void GameBaseImpl::OnPostRender(GPUContext* context, RenderContext& renderContext)
{
    // Handle missing splash screen texture case
    if (SplashScreen == nullptr)
    {
        OnSplashScreenEnd();
        return;
    }

    // Wait for texture loaded before showing splash screen
    if (!SplashScreen->IsLoaded() || SplashScreen.Get()->GetTexture()->MipLevels() != SplashScreen.Get()->StreamingTexture()->TotalMipLevels())
    {
        return;
    }
    const auto splashScreen = SplashScreen.Get()->GetTexture();

    // Configuration
    const float fadeTime = 0.5f;
    const float showTime = 1.0f;
    const float totalTime = fadeTime + showTime + fadeTime;

    // Update animation
    const auto deltaTime = static_cast<float>(Time::Draw.UnscaledDeltaTime.GetTotalSeconds());
    SplashScreenTime += deltaTime;
    if (SplashScreenTime >= totalTime)
    {
        OnSplashScreenEnd();
        return;
    }

    PROFILE_GPU_CPU_NAMED("Splash Screen");

    // Calculate visibility
    float fade = 1.0f;
    if (SplashScreenTime < fadeTime)
        fade = SplashScreenTime / fadeTime;
    else if (SplashScreenTime > fadeTime + showTime)
        fade = 1.0f - (SplashScreenTime - fadeTime - showTime) / fadeTime;

    // Calculate image area (fill screen, keep aspect ratio, and snap to pixels)
    const auto viewport = renderContext.Task->GetViewport();
    const Rectangle screenRect(viewport.X, viewport.Y, viewport.Width, viewport.Height);
    Rectangle imageArea = screenRect;
    imageArea.Scale(0.6f);
    const float aspectRatio = static_cast<float>(splashScreen->Width()) / static_cast<float>(splashScreen->Height());
    const float height = imageArea.GetWidth() / aspectRatio;
    imageArea.Location.Y += (imageArea.GetHeight() - height) * 0.5f;
    imageArea.Size.Y = height;
    imageArea.Location = Float2::Ceil(imageArea.Location);
    imageArea.Size = Float2::Ceil(imageArea.Size);

    // Draw
    Render2D::Begin(GPUDevice::Instance->GetMainContext(), renderContext.Task->GetOutputView(), nullptr, viewport);
    Render2D::FillRectangle(screenRect, Color::Black);
    Render2D::DrawTexture(splashScreen, imageArea, Color(1.0f, 1.0f, 1.0f, fade));
    Render2D::End();
}

void GameBaseImpl::OnSplashScreenEnd()
{
    // Hide splash screen
    SplashScreenTime = 0;
    SplashScreen = nullptr;
    MainRenderTask::Instance->PostRender.Unbind(&OnPostRender);

    // Load the first scene
    LOG(Info, "Loading the first scene");
    const auto sceneId = FirstScene ? FirstScene.GetID() : Guid::Empty;
    FirstScene = nullptr;
    if (Level::LoadSceneAsync(sceneId))
    {
        LOG(Fatal, "Cannot load the first scene.");
        return;
    }
}

#endif
