// Copyright (c) Wojciech Figat. All rights reserved.

#include "Time.h"
#include "EngineService.h"
#include "Engine/Core/Math/Math.h"
#include "Engine/Platform/Platform.h"
#include "Engine/Core/Config/TimeSettings.h"
#include "Engine/Serialization/Serialization.h"
#if !USE_EDITOR
#include "Engine/Engine/Engine.h"
#endif

namespace
{
    bool FixedDeltaTimeEnable;
    float FixedDeltaTimeValue;
    float MaxUpdateDeltaTime = 0.1f;
#if USE_EDITOR
    constexpr bool _gamePausedUnfocsed = false;
    #define GetFps(fps) fps
#else
    bool _gamePausedUnfocsed = false;
    float UnfocusedMaxFPS = 0.0f;
    bool UnfocusedPause = false;
    float GetFps(float fps)
    {
        if (UnfocusedMaxFPS > 0 && !Engine::HasFocus && fps > UnfocusedMaxFPS)
            fps = UnfocusedMaxFPS;
        return fps;
    }
#endif
}

bool Time::_gamePaused = false;
float Time::_physicsMaxDeltaTime = 0.1f;
DateTime Time::StartupTime;
float Time::UpdateFPS = 60.0f;
float Time::PhysicsFPS = 60.0f;
float Time::DrawFPS = 60.0f;
float Time::TimeScale = 1.0f;
Time::TickData Time::Update;
Time::FixedStepTickData Time::Physics;
Time::TickData Time::Draw;
Time::TickData* Time::Current = nullptr;

class TimeService : public EngineService
{
public:

    TimeService()
        : EngineService(TEXT("Time"), -850)
    {
        FixedDeltaTimeEnable = false;
        FixedDeltaTimeValue = 0.0f;

#if USE_EDITOR
        // Disable gameplay in Editor on startup
        Time::_gamePaused = true;
#endif
    }
};

TimeService TimeServiceInstance;

void TimeSettings::Apply()
{
    Time::UpdateFPS = UpdateFPS;
    Time::PhysicsFPS = PhysicsFPS;
    Time::DrawFPS = DrawFPS;
    Time::TimeScale = TimeScale;
    ::MaxUpdateDeltaTime = MaxUpdateDeltaTime;
#if !USE_EDITOR
    ::UnfocusedMaxFPS = UnfocusedMaxFPS;
    ::UnfocusedPause = UnfocusedPause;
#endif
}

void Time::TickData::Synchronize(float targetFps, double currentTime)
{
    OnReset(targetFps, currentTime);
    Time = UnscaledTime = TimeSpan::Zero();
    NextBegin = targetFps > ZeroTolerance ? LastBegin + (1.0f / targetFps) : 0.0;
}

void Time::TickData::OnReset(float targetFps, double currentTime)
{
    DeltaTime = UnscaledDeltaTime = targetFps > ZeroTolerance ? TimeSpan::FromSeconds(1.0f / targetFps) : TimeSpan::Zero();
    LastLength = static_cast<double>(DeltaTime.Ticks) / TimeSpan::TicksPerSecond;
    LastBegin = currentTime - LastLength;
    LastEnd = currentTime;
}

bool Time::TickData::OnTickBegin(double time, float targetFps, float maxDeltaTime)
{
    // Check if can perform a tick
    double deltaTime;
    if (FixedDeltaTimeEnable)
    {
        deltaTime = (double)FixedDeltaTimeValue;
    }
    else
    {
        if (time < NextBegin)
            return false;

        deltaTime = Math::Max((time - LastBegin), 0.0);
        if (deltaTime > maxDeltaTime)
        {
            deltaTime = (double)maxDeltaTime;
            NextBegin = time;
        }

        if (targetFps > ZeroTolerance)
        {
            int skip = (int)(1 + (time - NextBegin) * targetFps);
            NextBegin += (1.0 / targetFps) * skip;
        }
    }

    // Update data
    Advance(time, deltaTime);

    return true;
}

void Time::TickData::OnTickEnd()
{
    const double time = Platform::GetTimeSeconds();
    LastEnd = time;
    LastLength = time - LastBegin;
}

void Time::TickData::Advance(double time, double deltaTime)
{
    float timeScale = TimeScale;
    if (_gamePaused || _gamePausedUnfocsed)
        timeScale = 0.0f;
    LastBegin = time;
    UnscaledDeltaTime = TimeSpan::FromSeconds(deltaTime);
    UnscaledTime += UnscaledDeltaTime;
    DeltaTime = TimeSpan::FromSeconds(deltaTime * (double)timeScale);
    Time += DeltaTime;
    TicksCount++;
}

bool Time::FixedStepTickData::OnTickBegin(double time, float targetFps, float maxDeltaTime)
{
    // Check if can perform a tick
    double deltaTime, minDeltaTime;
    if (FixedDeltaTimeEnable)
    {
        deltaTime = (double)FixedDeltaTimeValue;
        minDeltaTime = deltaTime;
    }
    else
    {
        if (time < NextBegin)
            return false;

        minDeltaTime = targetFps > ZeroTolerance ? 1.0 / targetFps : 0.0;
        deltaTime = Math::Max((time - LastBegin), 0.0);
        if (deltaTime > maxDeltaTime)
        {
            deltaTime = (double)maxDeltaTime;
            NextBegin = time;
        }

        if (targetFps > ZeroTolerance)
        {
            int skip = (int)(1 + (time - NextBegin) * targetFps);
            NextBegin += (1.0 / targetFps) * skip;
        }
    }
    Samples.Add(deltaTime);

    // Check if last few ticks were not taking too long so it's running slowly
    const bool isRunningSlowly = Samples.Average() > 1.5 * minDeltaTime;
    if (!isRunningSlowly)
    {
        // Make steps fixed size
        const double diff = deltaTime - minDeltaTime;
        time -= diff;
        deltaTime = minDeltaTime;
    }

    // Update data
    Advance(time, deltaTime);

    return true;
}

double Time::GetNextTick()
{
    const double nextUpdate = Time::Update.NextBegin;
    const double nextPhysics = Time::Physics.NextBegin;
    const double nextDraw = Time::Draw.NextBegin;

    double nextTick = MAX_double;
    if (UpdateFPS > ZeroTolerance && nextUpdate < nextTick)
        nextTick = nextUpdate;
    if (PhysicsFPS > ZeroTolerance && nextPhysics < nextTick)
        nextTick = nextPhysics;
    if (DrawFPS > ZeroTolerance && nextDraw < nextTick)
        nextTick = nextDraw;

    if (nextTick == MAX_double)
        return 0.0;
    return nextTick;
}

void Time::SetGamePaused(bool value)
{
    if (_gamePaused == value)
        return;
    _gamePaused = value;
    if (_gamePausedUnfocsed)
        return;

    // Reset ticking
    const double time = Platform::GetTimeSeconds();
    Update.OnReset(UpdateFPS, time);
    Physics.OnReset(PhysicsFPS, time);
    Draw.OnReset(DrawFPS, time);
}

float Time::GetDeltaTime()
{
    auto* data = Current ? Current : &Update;
    return data->DeltaTime.GetTotalSeconds();
}

float Time::GetGameTime()
{
    auto* data = Current ? Current : &Update;
    return data->Time.GetTotalSeconds();
}

float Time::GetUnscaledDeltaTime()
{
    auto* data = Current ? Current : &Update;
    return data->UnscaledDeltaTime.GetTotalSeconds();
}

float Time::GetUnscaledGameTime()
{
    auto* data = Current ? Current : &Update;
    return data->UnscaledTime.GetTotalSeconds();
}

float Time::GetTimeSinceStartup()
{
    return (DateTime::Now() - StartupTime).GetTotalSeconds();
}

void Time::SetFixedDeltaTime(bool enable, float value)
{
    FixedDeltaTimeEnable = enable;
    FixedDeltaTimeValue = value;
}

void Time::Synchronize()
{
    // Initialize tick data (based on a time settings)
    const double time = Platform::GetTimeSeconds();
    Update.Synchronize(UpdateFPS, time);
    Physics.Synchronize(PhysicsFPS, time);
    Draw.Synchronize(DrawFPS, time);
}

bool Time::OnBeginUpdate(double time)
{
#if !USE_EDITOR
    // Pause game if window lost focus (based on game settings)
    bool gamePausedUnfocsed = !Engine::HasFocus && UnfocusedPause;
    if (gamePausedUnfocsed != _gamePausedUnfocsed)
    {
        _gamePausedUnfocsed = gamePausedUnfocsed;
        if (!gamePausedUnfocsed)
        {
            // Reset ticking
            const double time = Platform::GetTimeSeconds();
            Update.OnReset(UpdateFPS, time);
            Physics.OnReset(PhysicsFPS, time);
            Draw.OnReset(DrawFPS, time);
        }
    }
#endif

    if (Update.OnTickBegin(time, GetFps(UpdateFPS), MaxUpdateDeltaTime))
    {
        Current = &Update;
        return true;
    }
    return false;
}

bool Time::OnBeginPhysics(double time)
{
    if (Physics.OnTickBegin(time, GetFps(PhysicsFPS), _physicsMaxDeltaTime))
    {
        Current = &Physics;
        return true;
    }
    return false;
}

bool Time::OnBeginDraw(double time)
{
    if (Draw.OnTickBegin(time, GetFps(DrawFPS), 1.0f))
    {
        Current = &Draw;
        return true;
    }
    return false;
}

void Time::OnEndUpdate()
{
    Update.OnTickEnd();
    Current = nullptr;
}

void Time::OnEndPhysics()
{
    Physics.OnTickEnd();
    Current = nullptr;
}

void Time::OnEndDraw()
{
    Draw.OnTickEnd();
    Current = nullptr;
}
