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

#include "Input.h"
#include "InputSettings.h"
#include "Keyboard.h"
#include "Mouse.h"
#include "Gamepad.h"
#include "Engine/Platform/Window.h"
#include "Engine/Engine/Engine.h"
#include "Engine/Engine/EngineService.h"
#include "Engine/Engine/Screen.h"
#include "Engine/Engine/Time.h"
#include "Engine/Platform/WindowsManager.h"
#include "Engine/Scripting/ScriptingType.h"
#include "Engine/Scripting/BinaryModule.h"
#include "Engine/Profiler/ProfilerCPU.h"
#include "Engine/Profiler/ProfilerMemory.h"
#include "Engine/Serialization/JsonTools.h"

struct AxisEvaluation
{
    float RawValue;
    float Value;
    float PrevKeyValue;
    bool Used;
};

struct ActionData
{
    bool Active = false;
    uint64 FrameIndex = 0;
    InputActionState State = InputActionState::Waiting;
};

struct AxisData
{
    float Value = 0.0f;
    float ValueRaw = 0.0f;
    float PrevValue = 0.0f;
    float PrevKeyValue = 0.0f;
    uint64 FrameIndex = 0;
};

namespace InputImpl
{
    Dictionary<String, ActionData> Actions;
    Dictionary<String, AxisData> Axes;
    bool GamepadsChanged = true;
    Array<AxisEvaluation> AxesValues;
    InputDevice::EventQueue InputEvents;
}

using namespace InputImpl;

class InputService : public EngineService
{
public:
    InputService()
        : EngineService(TEXT("Input"), -60)
    {
    }

    void Update() override;
    void Dispose() override;
};

InputService InputServiceInstance;

Mouse* Input::Mouse = nullptr;
Keyboard* Input::Keyboard = nullptr;
Array<Gamepad*, FixedAllocation<MAX_GAMEPADS>> Input::Gamepads;
Action Input::GamepadsChanged;
Array<InputDevice*, InlinedAllocation<16>> Input::CustomDevices;
Delegate<Char> Input::CharInput;
Delegate<KeyboardKeys> Input::KeyDown;
Delegate<KeyboardKeys> Input::KeyUp;
Delegate<const Float2&, MouseButton> Input::MouseDown;
Delegate<const Float2&, MouseButton> Input::MouseUp;
Delegate<const Float2&, MouseButton> Input::MouseDoubleClick;
Delegate<const Float2&, float> Input::MouseWheel;
Delegate<const Float2&> Input::MouseMove;
Delegate<const Float2&> Input::MouseMoveRelative;
Action Input::MouseLeave;
Delegate<InputGamepadIndex, GamepadButton> Input::GamepadButtonDown;
Delegate<InputGamepadIndex, GamepadButton> Input::GamepadButtonUp;
Delegate<const Float2&, int32> Input::TouchDown;
Delegate<const Float2&, int32> Input::TouchMove;
Delegate<const Float2&, int32> Input::TouchUp;
Delegate<StringView, InputActionState> Input::ActionTriggered;
Delegate<StringView> Input::AxisValueChanged;
Array<ActionConfig> Input::ActionMappings;
Array<AxisConfig> Input::AxisMappings;

void InputSettings::Apply()
{
    PROFILE_MEM(Input);
    Input::ActionMappings = ActionMappings;
    Input::AxisMappings = AxisMappings;
}

void InputSettings::Deserialize(DeserializeStream& stream, ISerializeModifier* modifier)
{
    PROFILE_MEM(Input);
    const auto actionMappings = stream.FindMember("ActionMappings");
    if (actionMappings != stream.MemberEnd())
    {
        auto& actionMappingsArray = actionMappings->value;
        if (actionMappingsArray.IsArray())
        {
            ActionMappings.Resize(actionMappingsArray.Size(), false);
            for (uint32 i = 0; i < actionMappingsArray.Size(); i++)
            {
                auto& v = actionMappingsArray[i];
                if (!v.IsObject())
                    continue;

                ActionConfig& config = ActionMappings[i];
                config.Name = JsonTools::GetString(v, "Name");
                config.Mode = JsonTools::GetEnum(v, "Mode", InputActionMode::Pressing);
                config.Key = JsonTools::GetEnum(v, "Key", KeyboardKeys::None);
                config.MouseButton = JsonTools::GetEnum(v, "MouseButton", MouseButton::None);
                config.GamepadButton = JsonTools::GetEnum(v, "GamepadButton", GamepadButton::None);
                config.Gamepad = JsonTools::GetEnum(v, "Gamepad", InputGamepadIndex::All);
                config.DeadZone = JsonTools::GetFloat(v, "DeadZone", 0.5f);
            }
        }
        else
        {
            ActionMappings.Resize(0, false);
        }
    }

    const auto axisMappings = stream.FindMember("AxisMappings");
    if (axisMappings != stream.MemberEnd())
    {
        auto& axisMappingsArray = axisMappings->value;
        if (axisMappingsArray.IsArray())
        {
            AxisMappings.Resize(axisMappingsArray.Size(), false);
            for (uint32 i = 0; i < axisMappingsArray.Size(); i++)
            {
                auto& v = axisMappingsArray[i];
                if (!v.IsObject())
                    continue;

                AxisConfig& config = AxisMappings[i];
                config.Name = JsonTools::GetString(v, "Name");
                config.Axis = JsonTools::GetEnum(v, "Axis", InputAxisType::MouseX);
                config.Gamepad = JsonTools::GetEnum(v, "Gamepad", InputGamepadIndex::All);
                config.PositiveButton = JsonTools::GetEnum(v, "PositiveButton", KeyboardKeys::None);
                config.NegativeButton = JsonTools::GetEnum(v, "NegativeButton", KeyboardKeys::None);
                config.GamepadPositiveButton = JsonTools::GetEnum(v, "GamepadPositiveButton", GamepadButton::None);
                config.GamepadNegativeButton = JsonTools::GetEnum(v, "GamepadNegativeButton", GamepadButton::None);
                config.DeadZone = JsonTools::GetFloat(v, "DeadZone", 0.1f);
                config.Sensitivity = JsonTools::GetFloat(v, "Sensitivity", 0.4f);
                config.Gravity = JsonTools::GetFloat(v, "Gravity", 1.0f);
                config.Scale = JsonTools::GetFloat(v, "Scale", 1.0f);
                config.Snap = JsonTools::GetBool(v, "Snap", false);
            }
        }
        else
        {
            AxisMappings.Resize(0, false);
        }
    }
}

void Mouse::OnMouseMoved(const Float2& newPosition)
{
    _prevState.MousePosition = newPosition;
    _state.MousePosition = newPosition;
}

void Mouse::OnMouseDown(const Float2& position, const MouseButton button, Window* target)
{
    PROFILE_MEM(Input);
    Event& e = _queue.AddOne();
    e.Type = EventType::MouseDown;
    e.Target = target;
    e.MouseData.Button = button;
    e.MouseData.Position = position;
}

bool Mouse::IsAnyButtonDown() const
{
    // TODO: optimize with SIMD
    bool result = false;
    for (auto e : Mouse::_state.MouseButtons)
        result |= e;
    return result;
}

void Mouse::OnMouseUp(const Float2& position, const MouseButton button, Window* target)
{
    PROFILE_MEM(Input);
    Event& e = _queue.AddOne();
    e.Type = EventType::MouseUp;
    e.Target = target;
    e.MouseData.Button = button;
    e.MouseData.Position = position;
}

void Mouse::OnMouseDoubleClick(const Float2& position, const MouseButton button, Window* target)
{
    PROFILE_MEM(Input);
    Event& e = _queue.AddOne();
    e.Type = EventType::MouseDoubleClick;
    e.Target = target;
    e.MouseData.Button = button;
    e.MouseData.Position = position;
}

void Mouse::OnMouseMove(const Float2& position, Window* target)
{
    PROFILE_MEM(Input);
    Event& e = _queue.AddOne();
    e.Type = EventType::MouseMove;
    e.Target = target;
    e.MouseData.Position = position;
}

void Mouse::OnMouseMoveRelative(const Float2& positionRelative, Window* target)
{
    Event& e = _queue.AddOne();
    e.Type = EventType::MouseMoveRelative;
    e.Target = target;
    e.MouseMovementData.PositionRelative = positionRelative;
}

void Mouse::OnMouseLeave(Window* target)
{
    PROFILE_MEM(Input);
    Event& e = _queue.AddOne();
    e.Type = EventType::MouseLeave;
    e.Target = target;
}

void Mouse::OnMouseWheel(const Float2& position, float delta, Window* target)
{
    PROFILE_MEM(Input);
    Event& e = _queue.AddOne();
    e.Type = EventType::MouseWheel;
    e.Target = target;
    e.MouseWheelData.WheelDelta = delta;
    e.MouseWheelData.Position = position;
}

void Mouse::ResetState()
{
    InputDevice::ResetState();

    _prevState.Clear();
    _state.Clear();
}

bool Mouse::Update(EventQueue& queue)
{
    // Move the current state to the previous
    Platform::MemoryCopy(&_prevState, &_state, sizeof(State));

    // Gather new events
    if (UpdateState())
        return true;

    // Handle events
    _state.MouseWheelDelta = 0;
    for (int32 i = 0; i < _queue.Count(); i++)
    {
        const Event& e = _queue[i];
        switch (e.Type)
        {
        case EventType::MouseDown:
        {
            _state.MouseButtons[static_cast<int32>(e.MouseData.Button)] = true;
            break;
        }
        case EventType::MouseUp:
        {
            _state.MouseButtons[static_cast<int32>(e.MouseData.Button)] = false;
            break;
        }
        case EventType::MouseDoubleClick:
        {
            _state.MouseButtons[static_cast<int32>(e.MouseData.Button)] = true;
            break;
        }
        case EventType::MouseWheel:
        {
            _state.MouseWheelDelta += e.MouseWheelData.WheelDelta;
            break;
        }
        case EventType::MouseMove:
        {
            _state.MousePosition = e.MouseData.Position;
            break;
        }
        case EventType::MouseMoveRelative:
        {
            _state.MousePosition += e.MouseMovementData.PositionRelative;
            break;
        }
        case EventType::MouseLeave:
        {
            break;
        }
        }
    }

    // Send events further
    queue.Add(_queue);
    _queue.Clear();
    return false;
}

void Keyboard::State::Clear()
{
    Platform::MemoryClear(this, sizeof(State));
}

Keyboard::Keyboard()
    : InputDevice(SpawnParams(Guid::New(), TypeInitializer), TEXT("Keyboard"))
{
    _state.Clear();
    _prevState.Clear();
}

bool Keyboard::IsAnyKeyDown() const
{
    // TODO: optimize with SIMD
    bool result = false;
    for (auto e : _state.Keys)
        result |= e;
    return result;
}

void Keyboard::OnCharInput(Char c, Window* target)
{
    // Skip control characters
    if (c < 32)
        return;

    PROFILE_MEM(Input);
    Event& e = _queue.AddOne();
    e.Type = EventType::Char;
    e.Target = target;
    e.CharData.Char = c;
}

void Keyboard::OnKeyUp(KeyboardKeys key, Window* target)
{
    if (key >= KeyboardKeys::MAX)
        return;
    PROFILE_MEM(Input);
    Event& e = _queue.AddOne();
    e.Type = EventType::KeyUp;
    e.Target = target;
    e.KeyData.Key = key;
}

void Keyboard::OnKeyDown(KeyboardKeys key, Window* target)
{
    if (key >= KeyboardKeys::MAX)
        return;
    PROFILE_MEM(Input);
    Event& e = _queue.AddOne();
    e.Type = EventType::KeyDown;
    e.Target = target;
    e.KeyData.Key = key;
}

void Keyboard::ResetState()
{
    InputDevice::ResetState();

    _prevState.Clear();
    _state.Clear();
}

bool Keyboard::Update(EventQueue& queue)
{
    // Move the current state to the previous
    Platform::MemoryCopy(&_prevState, &_state, sizeof(State));

    // Gather new events
    if (UpdateState())
        return true;

    // Handle events
    _state.InputTextLength = 0;
    for (int32 i = 0; i < _queue.Count(); i++)
    {
        const Event& e = _queue[i];
        switch (e.Type)
        {
        case EventType::Char:
        {
            if (_state.InputTextLength < ARRAY_COUNT(_state.InputText) - 1)
                _state.InputText[_state.InputTextLength++] = e.CharData.Char;
            break;
        }
        case EventType::KeyDown:
        {
            _state.Keys[static_cast<int32>(e.KeyData.Key)] = true;
            break;
        }
        case EventType::KeyUp:
        {
            _state.Keys[static_cast<int32>(e.KeyData.Key)] = false;
            break;
        }
        }
    }

    // Send events further
    queue.Add(_queue);
    _queue.Clear();
    return false;
}

int32 Input::GetGamepadsCount()
{
    return Gamepads.Count();
}

Gamepad* Input::GetGamepad(int32 index)
{
    if (index >= 0 && index < Gamepads.Count())
        return Gamepads[index];
    return nullptr;
}

void Input::OnGamepadsChanged()
{
    ::GamepadsChanged = true;
}

StringView Input::GetInputText()
{
    return Keyboard ? Keyboard->GetInputText() : StringView::Empty;
}

bool Input::GetKey(const KeyboardKeys key)
{
    return Keyboard ? Keyboard->GetKey(key) : false;
}

bool Input::GetKeyDown(const KeyboardKeys key)
{
    return Keyboard ? Keyboard->GetKeyDown(key) : false;
}

bool Input::GetKeyUp(const KeyboardKeys key)
{
    return Keyboard ? Keyboard->GetKeyUp(key) : false;
}

Float2 Input::GetMousePosition()
{
    return Mouse ? Screen::ScreenToGameViewport(Mouse->GetPosition()) : Float2::Minimum;
}

void Input::SetMousePosition(const Float2& position)
{
    if (Mouse && Engine::HasGameViewportFocus())
    {
        const auto pos = Screen::GameViewportToScreen(position);
        if (pos > Float2::Minimum)
            Mouse->SetMousePosition(pos);
    }
}

Float2 Input::GetMouseScreenPosition()
{
    return Mouse ? Mouse->GetPosition() : Float2::Minimum;
}

void Input::SetMouseScreenPosition(const Float2& position)
{
    if (Mouse && Engine::HasFocus)
    {
        Mouse->SetMousePosition(position);
    }
}

Float2 Input::GetMousePositionDelta()
{
    return Mouse ? Mouse->GetPositionDelta() : Float2::Zero;
}

float Input::GetMouseScrollDelta()
{
    return Mouse ? Mouse->GetScrollDelta() : 0.0f;
}

bool Input::GetMouseButton(const MouseButton button)
{
    return Mouse ? Mouse->GetButton(button) : false;
}

bool Input::GetMouseButtonDown(const MouseButton button)
{
    return Mouse ? Mouse->GetButtonDown(button) : false;
}

bool Input::GetMouseButtonUp(const MouseButton button)
{
    return Mouse ? Mouse->GetButtonUp(button) : false;
}

float Input::GetGamepadAxis(int32 gamepadIndex, GamepadAxis axis)
{
    if (gamepadIndex >= 0 && gamepadIndex < Gamepads.Count())
        return Gamepads[gamepadIndex]->GetAxis(axis);
    return 0.0f;
}

bool Input::GetGamepadButton(int32 gamepadIndex, GamepadButton button, float deadZone)
{
    if (gamepadIndex >= 0 && gamepadIndex < Gamepads.Count())
        return Gamepads[gamepadIndex]->GetButton(button, deadZone);
    return false;
}

bool Input::GetGamepadButtonDown(int32 gamepadIndex, GamepadButton button, float deadZone)
{
    if (gamepadIndex >= 0 && gamepadIndex < Gamepads.Count())
        return Gamepads[gamepadIndex]->GetButtonDown(button, deadZone);
    return false;
}

bool Input::GetGamepadButtonUp(int32 gamepadIndex, GamepadButton button, float deadZone)
{
    if (gamepadIndex >= 0 && gamepadIndex < Gamepads.Count())
        return Gamepads[gamepadIndex]->GetButtonUp(button, deadZone);
    return false;
}

float Input::GetGamepadAxis(InputGamepadIndex gamepad, GamepadAxis axis)
{
    if (gamepad == InputGamepadIndex::All)
    {
        float result = 0.0f;
        for (auto g : Gamepads)
        {
            float v = g->GetAxis(axis);
            if (Math::Abs(v) > Math::Abs(result))
                result = v;
        }
        return result;
    }
    else
    {
        const auto index = static_cast<int32>(gamepad);
        if (index < Gamepads.Count())
            return Gamepads[index]->GetAxis(axis);
    }
    return false;
}

bool Input::GetGamepadButton(InputGamepadIndex gamepad, GamepadButton button, float deadZone)
{
    if (gamepad == InputGamepadIndex::All)
    {
        for (auto g : Gamepads)
        {
            if (g->GetButton(button, deadZone))
                return true;
        }
    }
    else
    {
        const auto index = static_cast<int32>(gamepad);
        if (index < Gamepads.Count())
            return Gamepads[index]->GetButton(button, deadZone);
    }
    return false;
}

bool Input::GetGamepadButtonDown(InputGamepadIndex gamepad, GamepadButton button, float deadZone)
{
    if (gamepad == InputGamepadIndex::All)
    {
        for (auto g : Gamepads)
        {
            if (g->GetButtonDown(button, deadZone))
                return true;
        }
    }
    else
    {
        const auto index = static_cast<int32>(gamepad);
        if (index < Gamepads.Count())
            return Gamepads[index]->GetButtonDown(button, deadZone);
    }
    return false;
}

bool Input::GetGamepadButtonUp(InputGamepadIndex gamepad, GamepadButton button, float deadZone)
{
    if (gamepad == InputGamepadIndex::All)
    {
        for (auto g : Gamepads)
        {
            if (g->GetButtonUp(button, deadZone))
                return true;
        }
    }
    else
    {
        const auto index = static_cast<int32>(gamepad);
        if (index < Gamepads.Count())
            return Gamepads[index]->GetButtonUp(button, deadZone);
    }
    return false;
}

bool Input::GetAction(const StringView& name)
{
    const auto e = Actions.TryGet(name);
    return e ? e->Active : false;
}

InputActionState Input::GetActionState(const StringView& name)
{
    const auto e = Actions.TryGet(name);
    if (e != nullptr)
    {
        return e->State;
    }
    return InputActionState::None;
}

float Input::GetAxis(const StringView& name)
{
    const auto e = Axes.TryGet(name);
    return e ? e->Value : false;
}

float Input::GetAxisRaw(const StringView& name)
{
    const auto e = Axes.TryGet(name);
    return e ? e->ValueRaw : false;
}

void Input::SetInputMappingFromSettings(const JsonAssetReference<InputSettings>& settings)
{
    PROFILE_MEM(Input);
    auto actionMappings = settings.GetInstance()->ActionMappings;
    ActionMappings.Resize(actionMappings.Count(), false);
    for (int i = 0; i < actionMappings.Count(); i++)
    {
        ActionMappings[i] = actionMappings.At(i);
    }

    auto axisMappings = settings.GetInstance()->AxisMappings;
    AxisMappings.Resize(axisMappings.Count(), false);
    for (int i = 0; i < axisMappings.Count(); i++)
    {
        AxisMappings[i] = axisMappings.At(i);
    }
    Axes.Clear();
    Actions.Clear();
}

void Input::SetInputMappingToDefaultSettings()
{
    PROFILE_MEM(Input);
    InputSettings* settings = InputSettings::Get();
    if (settings)
    {
        ActionMappings = settings->ActionMappings;
        AxisMappings = settings->AxisMappings;
        Axes.Clear();
        Actions.Clear();
    }
}

ActionConfig Input::GetActionConfigByName(const StringView& name)
{
    ActionConfig config = {};
    for (const auto& a : ActionMappings)
    {
        if (a.Name == name)
        {
            config = a;
            return config;
        }
    }
    return config;
}

Array<ActionConfig> Input::GetAllActionConfigsByName(const StringView& name)
{
    Array<ActionConfig> actionConfigs;
    for (const auto& a : ActionMappings)
    {
        if (a.Name == name)
            actionConfigs.Add(a);
    }
    return actionConfigs;
}

AxisConfig Input::GetAxisConfigByName(const StringView& name)
{
    AxisConfig config = {};
    for (const auto& a : AxisMappings)
    {
        if (a.Name == name)
        {
            config = a;
            return config;
        }
    }
    return config;
}

Array<AxisConfig> Input::GetAllAxisConfigsByName(const StringView& name)
{
    Array<AxisConfig> actionConfigs;
    for (const auto& a : AxisMappings)
    {
        if (a.Name == name)
            actionConfigs.Add(a);
    }
    return actionConfigs;
}

void Input::SetAxisConfigByName(const StringView& name, AxisConfig& config, bool all)
{
    PROFILE_MEM(Input);
    for (int i = 0; i < AxisMappings.Count(); ++i)
    {
        auto& mapping = AxisMappings.At(i);
        if (mapping.Name.Compare(name.ToString()) == 0)
        {
            if (config.Name.IsEmpty())
                config.Name = name;
            mapping = config;
            if (!all)
                break;
        }
    }
}

void Input::SetAxisConfigByName(const StringView& name, InputAxisType inputType, const KeyboardKeys positiveButton, const KeyboardKeys negativeButton, bool all)
{
    PROFILE_MEM(Input);
    for (int i = 0; i < AxisMappings.Count(); ++i)
    {
        auto& mapping = AxisMappings.At(i);
        if (mapping.Name.Compare(name.ToString()) == 0 && mapping.Axis == inputType)
        {
            mapping.PositiveButton = positiveButton;
            mapping.NegativeButton = negativeButton;
            if (!all)
                break;
        }
    }
}

void Input::SetAxisConfigByName(const StringView& name, InputAxisType inputType, const GamepadButton positiveButton, const GamepadButton negativeButton, InputGamepadIndex gamepadIndex, bool all)
{
    PROFILE_MEM(Input);
    for (int i = 0; i < AxisMappings.Count(); ++i)
    {
        auto& mapping = AxisMappings.At(i);
        if (mapping.Name.Compare(name.ToString()) == 0 && mapping.Gamepad == gamepadIndex && mapping.Axis == inputType)
        {
            mapping.GamepadPositiveButton = positiveButton;
            mapping.GamepadNegativeButton = negativeButton;
            if (!all)
                break;
        }
    }
}

void Input::SetAxisConfigByName(const StringView& name, InputAxisType inputType, const float gravity, const float deadZone, const float sensitivity, const float scale, const bool snap, bool all)
{
    PROFILE_MEM(Input);
    for (int i = 0; i < AxisMappings.Count(); ++i)
    {
        auto& mapping = AxisMappings.At(i);
        if (mapping.Name.Compare(name.ToString()) == 0 && mapping.Axis == inputType)
        {
            mapping.Gravity = gravity;
            mapping.DeadZone = deadZone;
            mapping.Sensitivity = sensitivity;
            mapping.Scale = scale;
            mapping.Snap = snap;
            if (!all)
                break;
        }
    }
}

void Input::SetActionConfigByName(const StringView& name, const KeyboardKeys key, bool all)
{
    PROFILE_MEM(Input);
    for (int i = 0; i < ActionMappings.Count(); ++i)
    {
        auto& mapping = ActionMappings.At(i);
        if (mapping.Name.Compare(name.ToString()) == 0)
        {
            mapping.Key = key;
            if (!all)
                break;
        }
    }
}

void Input::SetActionConfigByName(const StringView& name, const MouseButton mouseButton, bool all)
{
    PROFILE_MEM(Input);
    for (int i = 0; i < ActionMappings.Count(); ++i)
    {
        auto& mapping = ActionMappings.At(i);
        if (mapping.Name.Compare(name.ToString()) == 0)
        {
            mapping.MouseButton = mouseButton;
            if (!all)
                break;
        }
    }
}

void Input::SetActionConfigByName(const StringView& name, const GamepadButton gamepadButton, InputGamepadIndex gamepadIndex, bool all)
{
    PROFILE_MEM(Input);
    for (int i = 0; i < ActionMappings.Count(); ++i)
    {
        auto& mapping = ActionMappings.At(i);
        if (mapping.Name.Compare(name.ToString()) == 0 && mapping.Gamepad == gamepadIndex)
        {
            mapping.GamepadButton = gamepadButton;
            if (!all)
                break;
        }
    }
}

void Input::SetActionConfigByName(const StringView& name, ActionConfig& config, bool all)
{
    PROFILE_MEM(Input);
    for (int i = 0; i < ActionMappings.Count(); ++i)
    {
        auto& mapping = ActionMappings.At(i);
        if (mapping.Name.Compare(name.ToString()) == 0)
        {
            if (config.Name.IsEmpty())
                config.Name = name;
            mapping = config;
            if (!all)
                break;
        }
    }
}

void InputService::Update()
{
    PROFILE_CPU();
    PROFILE_MEM(Input);
    const auto frame = Time::Update.TicksCount;
    const auto dt = Time::Update.UnscaledDeltaTime.GetTotalSeconds();
    InputEvents.Clear();

    // If application has no user focus then simply clear the state
    if (!Engine::HasFocus)
    {
        if (Input::Mouse)
            Input::Mouse->ResetState();
        if (Input::Keyboard)
            Input::Keyboard->ResetState();
        for (int32 i = 0; i < Input::Gamepads.Count(); i++)
            Input::Gamepads[i]->ResetState();
        Axes.Clear();
        Actions.Clear();
        return;
    }

    // Update input devices state
    if (Input::Mouse)
    {
        if (Input::Mouse->Update(InputEvents))
        {
            Input::Mouse->DeleteObject();
            Input::Mouse = nullptr;
        }
    }
    if (Input::Keyboard)
    {
        if (Input::Keyboard->Update(InputEvents))
        {
            Input::Keyboard->DeleteObject();
            Input::Keyboard = nullptr;
        }
    }
    for (int32 i = 0; i < Input::Gamepads.Count(); i++)
    {
        if (Input::Gamepads[i]->Update(InputEvents))
        {
            Input::Gamepads[i]->DeleteObject();
            Input::Gamepads.RemoveAtKeepOrder(i);
            Input::OnGamepadsChanged();
            i--;
            if (Input::Gamepads.IsEmpty())
                break;
        }
    }
    for (int32 i = 0; i < Input::CustomDevices.Count(); i++)
    {
        if (Input::CustomDevices[i]->Update(InputEvents))
        {
            Input::CustomDevices[i]->DeleteObject();
            Input::CustomDevices.RemoveAtKeepOrder(i);
            i--;
            if (Input::CustomDevices.IsEmpty())
                break;
        }
    }

    // Send gamepads change events
    if (GamepadsChanged)
    {
        GamepadsChanged = false;
        Input::GamepadsChanged();
    }

    // Pick the first focused window for input events
    WindowsManager::WindowsLocker.Lock();
    Window* defaultWindow = nullptr;
    for (auto window : WindowsManager::Windows)
    {
        if (window->IsFocused() && window->GetSettings().AllowInput)
        {
            defaultWindow = window;
            break;
        }
    }
#if PLATFORM_SDL
    WindowsManager::WindowsLocker.Unlock();
#endif

    // Send input events for the focused window
    for (const auto& e : InputEvents)
    {
        auto window = e.Target ? e.Target : defaultWindow;
        if (!window || window->IsClosed())
            continue;
        switch (e.Type)
        {
        // Keyboard events
        case InputDevice::EventType::Char:
            window->OnCharInput(e.CharData.Char);
            break;
        case InputDevice::EventType::KeyDown:
            window->OnKeyDown(e.KeyData.Key);
            break;
        case InputDevice::EventType::KeyUp:
            window->OnKeyUp(e.KeyData.Key);
            break;
        // Mouse events
        case InputDevice::EventType::MouseDown:
            window->OnMouseDown(window->ScreenToClient(e.MouseData.Position), e.MouseData.Button);
            break;
        case InputDevice::EventType::MouseUp:
            window->OnMouseUp(window->ScreenToClient(e.MouseData.Position), e.MouseData.Button);
            break;
        case InputDevice::EventType::MouseDoubleClick:
            window->OnMouseDoubleClick(window->ScreenToClient(e.MouseData.Position), e.MouseData.Button);
            break;
        case InputDevice::EventType::MouseWheel:
            window->OnMouseWheel(window->ScreenToClient(e.MouseWheelData.Position), e.MouseWheelData.WheelDelta);
            break;
        case InputDevice::EventType::MouseMove:
            window->OnMouseMove(window->ScreenToClient(e.MouseData.Position));
            break;
        case InputDevice::EventType::MouseMoveRelative:
            window->OnMouseMoveRelative(e.MouseMovementData.PositionRelative);
            break;
        case InputDevice::EventType::MouseLeave:
            window->OnMouseLeave();
            break;
        // Touch events
        case InputDevice::EventType::TouchDown:
            window->OnTouchDown(window->ScreenToClient(e.TouchData.Position), e.TouchData.PointerId);
            break;
        case InputDevice::EventType::TouchMove:
            window->OnTouchMove(window->ScreenToClient(e.TouchData.Position), e.TouchData.PointerId);
            break;
        case InputDevice::EventType::TouchUp:
            window->OnTouchUp(window->ScreenToClient(e.TouchData.Position), e.TouchData.PointerId);
            break;
        }
    }
#if !PLATFORM_SDL
    WindowsManager::WindowsLocker.Unlock();
#endif

    // Skip if game has no focus to handle the input
    if (!Engine::HasGameViewportFocus())
    {
        Axes.Clear();
        Actions.Clear();
        return;
    }

    // Send input events
    for (const auto& e : InputEvents)
    {
        switch (e.Type)
        {
        // Keyboard events
        case InputDevice::EventType::Char:
            Input::CharInput(e.CharData.Char);
            break;
        case InputDevice::EventType::KeyDown:
            Input::KeyDown(e.KeyData.Key);
            break;
        case InputDevice::EventType::KeyUp:
            Input::KeyUp(e.KeyData.Key);
            break;
        // Mouse events
        case InputDevice::EventType::MouseDown:
            Input::MouseDown(e.MouseData.Position, e.MouseData.Button);
            break;
        case InputDevice::EventType::MouseUp:
            Input::MouseUp(e.MouseData.Position, e.MouseData.Button);
            break;
        case InputDevice::EventType::MouseDoubleClick:
            Input::MouseDoubleClick(e.MouseData.Position, e.MouseData.Button);
            break;
        case InputDevice::EventType::MouseWheel:
            Input::MouseWheel(e.MouseWheelData.Position, e.MouseWheelData.WheelDelta);
            break;
        case InputDevice::EventType::MouseMove:
            Input::MouseMove(e.MouseData.Position);
            break;
        case InputDevice::EventType::MouseMoveRelative:
            Input::MouseMoveRelative(e.MouseMovementData.PositionRelative);
            break;
        case InputDevice::EventType::MouseLeave:
            Input::MouseLeave();
            break;
        // Touch events
        case InputDevice::EventType::TouchDown:
            Input::TouchDown(e.TouchData.Position, e.TouchData.PointerId);
            break;
        case InputDevice::EventType::TouchMove:
            Input::TouchMove(e.TouchData.Position, e.TouchData.PointerId);
            break;
        case InputDevice::EventType::TouchUp:
            Input::TouchUp(e.TouchData.Position, e.TouchData.PointerId);
            break;
        }
    }
    // TODO: route gamepad button events into global InputEvents queue to improve processing
    for (int32 i = 0; i < Input::Gamepads.Count(); i++)
    {
        auto gamepad = Input::Gamepads[i];
        for (int32 buttonIdx = 1; buttonIdx < (int32)GamepadButton::MAX; buttonIdx++)
        {
            GamepadButton button = (GamepadButton)buttonIdx;
            if (gamepad->GetButtonDown(button))
                Input::GamepadButtonDown((InputGamepadIndex)i, button);
            else if (gamepad->GetButtonUp(button))
                Input::GamepadButtonUp((InputGamepadIndex)i, button);
        }
    }

    // Update all actions
    for (int32 i = 0; i < Input::ActionMappings.Count(); i++)
    {
        const auto& config = Input::ActionMappings[i];
        const StringView name = config.Name;
        ActionData& data = Actions[name];

        data.Active = false;
        data.State = InputActionState::Waiting;

        // Mark as updated in this frame
        data.FrameIndex = frame;
    }
    for (int32 i = 0; i < Input::ActionMappings.Count(); i++)
    {
        const auto& config = Input::ActionMappings[i];
        const StringView name = config.Name;
        ActionData& data = Actions[name];

        bool isActive;
        if (config.Mode == InputActionMode::Pressing)
        {
            isActive = Input::GetKey(config.Key) || Input::GetMouseButton(config.MouseButton) || Input::GetGamepadButton(config.Gamepad, config.GamepadButton, config.DeadZone);
        }
        else if (config.Mode == InputActionMode::Press)
        {
            isActive = Input::GetKeyDown(config.Key) || Input::GetMouseButtonDown(config.MouseButton) || Input::GetGamepadButtonDown(config.Gamepad, config.GamepadButton, config.DeadZone);
        }
        else
        {
            isActive = Input::GetKeyUp(config.Key) || Input::GetMouseButtonUp(config.MouseButton) || Input::GetGamepadButtonUp(config.Gamepad, config.GamepadButton, config.DeadZone);
        }

        if (Input::GetKeyDown(config.Key) || Input::GetMouseButtonDown(config.MouseButton) || Input::GetGamepadButtonDown(config.Gamepad, config.GamepadButton, config.DeadZone))
        {
            data.State = InputActionState::Press;
        }
        else if (Input::GetKey(config.Key) || Input::GetMouseButton(config.MouseButton) || Input::GetGamepadButton(config.Gamepad, config.GamepadButton, config.DeadZone))
        {
            data.State = InputActionState::Pressing;
        }
        else if (Input::GetKeyUp(config.Key) || Input::GetMouseButtonUp(config.MouseButton) || Input::GetGamepadButtonUp(config.Gamepad, config.GamepadButton, config.DeadZone))
        {
            data.State = InputActionState::Release;
        }

        data.Active |= isActive;
    }

    // Update all axes
    AxesValues.Resize(Input::AxisMappings.Count(), false);
    for (int32 i = 0; i < Input::AxisMappings.Count(); i++)
    {
        const auto& config = Input::AxisMappings[i];
        const StringView name = config.Name;
        const AxisData& data = Axes[name];

        // Get key raw value
        const bool isPositiveKey = Input::GetKey(config.PositiveButton) || Input::GetGamepadButton(config.Gamepad, config.GamepadPositiveButton);
        const bool isNegativeKey = Input::GetKey(config.NegativeButton) || Input::GetGamepadButton(config.Gamepad, config.GamepadNegativeButton);
        float keyRawValue = 0;
        if (isPositiveKey && !isNegativeKey)
        {
            keyRawValue = 1;
        }
        else if (!isPositiveKey && isNegativeKey)
        {
            keyRawValue = -1;
        }

        // Apply keyboard curve smoothing and snapping
        float prevKeyValue = data.PrevKeyValue;
        if (config.Snap && Math::NotSameSign(data.PrevKeyValue, keyRawValue))
        {
            prevKeyValue = 0;
        }
        float keyValue;
        if (Math::Abs(prevKeyValue) <= Math::Abs(keyRawValue))
        {
            keyValue = Math::LerpStable(prevKeyValue, keyRawValue, Math::Saturate(dt * config.Sensitivity));
        }
        else
        {
            keyValue = Math::LerpStable(prevKeyValue, keyRawValue, Math::Saturate(dt * config.Gravity));
        }

        // Get axis raw value
        float axisRawValue = 0.0f;
        switch (config.Axis)
        {
        case InputAxisType::MouseX:
            axisRawValue = Input::GetMousePositionDelta().X * config.Sensitivity;
            break;
        case InputAxisType::MouseY:
            axisRawValue = Input::GetMousePositionDelta().Y * config.Sensitivity;
            break;
        case InputAxisType::MouseWheel:
            axisRawValue = Input::GetMouseScrollDelta() * config.Sensitivity;
            break;
        case InputAxisType::GamepadLeftStickX:
            axisRawValue = Input::GetGamepadAxis(config.Gamepad, GamepadAxis::LeftStickX);
            break;
        case InputAxisType::GamepadLeftStickY:
            axisRawValue = Input::GetGamepadAxis(config.Gamepad, GamepadAxis::LeftStickY);
            break;
        case InputAxisType::GamepadRightStickX:
            axisRawValue = Input::GetGamepadAxis(config.Gamepad, GamepadAxis::RightStickX);
            break;
        case InputAxisType::GamepadRightStickY:
            axisRawValue = Input::GetGamepadAxis(config.Gamepad, GamepadAxis::RightStickY);
            break;
        case InputAxisType::GamepadLeftTrigger:
            axisRawValue = Input::GetGamepadAxis(config.Gamepad, GamepadAxis::LeftTrigger);
            break;
        case InputAxisType::GamepadRightTrigger:
            axisRawValue = Input::GetGamepadAxis(config.Gamepad, GamepadAxis::RightTrigger);
            break;
        case InputAxisType::GamepadDPadX:
            if (Input::GetGamepadButton(config.Gamepad, GamepadButton::DPadRight))
                axisRawValue = 1;
            else if (Input::GetGamepadButton(config.Gamepad, GamepadButton::DPadLeft))
                axisRawValue = -1;
            break;
        case InputAxisType::GamepadDPadY:
            if (Input::GetGamepadButton(config.Gamepad, GamepadButton::DPadUp))
                axisRawValue = 1;
            else if (Input::GetGamepadButton(config.Gamepad, GamepadButton::DPadDown))
                axisRawValue = -1;
            break;
        }

        // Apply dead zone
        const float deadZone = config.DeadZone;
        float axisValue = axisRawValue >= deadZone || axisRawValue <= -deadZone ? axisRawValue : 0.0f;
        keyValue = keyValue >= deadZone || keyValue <= -deadZone ? keyValue : 0.0f;

        auto& e = AxesValues[i];
        e.Used = false;
        e.PrevKeyValue = keyRawValue;

        // Select keyboard input or axis input (choose the higher absolute values)
        e.Value = Math::Abs(keyValue) > Math::Abs(axisValue) ? keyValue : axisValue;
        e.RawValue = Math::Abs(keyRawValue) > Math::Abs(axisRawValue) ? keyRawValue : axisRawValue;

        // Scale
        e.Value *= config.Scale;
    }
    for (int32 i = 0; i < Input::AxisMappings.Count(); i++)
    {
        auto& e = AxesValues[i];
        if (e.Used)
            continue;
        const auto& config = Input::AxisMappings[i];
        const StringView name = config.Name;
        AxisData& data = Axes[name];

        // Blend final axis raw value between all entries
        // Virtual axis with the same name may be used more than once, select the highest absolute value
        for (int32 j = i + 1; j < Input::AxisMappings.Count(); j++)
        {
            auto& other = AxesValues[j];
            if (!other.Used && Input::AxisMappings[j].Name == config.Name)
            {
                if (Math::Abs(other.Value) > Math::Abs(e.Value))
                {
                    e = other;
                }
                other.Used = true;
            }
        }

        // Setup axis data
        data.PrevKeyValue = e.PrevKeyValue;
        data.PrevValue = data.Value;
        data.ValueRaw = e.RawValue;
        data.Value = e.Value;

        // Mark as updated in this frame
        data.FrameIndex = frame;
    }

    // Remove not used entries
    for (auto i = Actions.Begin(); i.IsNotEnd(); ++i)
    {
        if (i->Value.FrameIndex != frame)
        {
            Actions.Remove(i);
        }
    }
    for (auto i = Axes.Begin(); i.IsNotEnd(); ++i)
    {
        if (i->Value.FrameIndex != frame)
        {
            Axes.Remove(i);
        }
    }

#if !PLATFORM_SDL
    // Lock mouse if need to
    const auto lockMode = Screen::GetCursorLock();
    if (lockMode == CursorLockMode::Locked)
    {
        const Float2 pos = Screen::ScreenToGameViewport(Screen::GetSize() * 0.5f) - 
                           Screen::ScreenToGameViewport(Float2::Zero);
        Input::SetMousePosition(pos);
    }
#endif

    // Send events for the active actions and axes (send events only in play mode)
    if (!Time::GetGamePaused())
    {
        for (auto i = Axes.Begin(); i.IsNotEnd(); ++i)
        {
            if (i->Value.Value != i->Value.PrevValue)
            {
                Input::AxisValueChanged(i->Key);
            }
        }
        
        for (auto i = Actions.Begin(); i.IsNotEnd(); ++i)
        {
            if (i->Value.State != InputActionState::Waiting)
            {
                Input::ActionTriggered(i->Key, i->Value.State);
            }
        }
    }
}

void InputService::Dispose()
{
    // Dispose input devices
    if (Input::Mouse)
    {
        Input::Mouse->DeleteObject();
        Input::Mouse = nullptr;
    }
    if (Input::Keyboard)
    {
        Input::Keyboard->DeleteObject();
        Input::Keyboard = nullptr;
    }
    for (int32 i = 0; i < Input::Gamepads.Count(); i++)
        Input::Gamepads[i]->DeleteObject();
    Input::Gamepads.Clear();
    for (int32 i = 0; i < Input::CustomDevices.Count(); i++)
        Input::CustomDevices[i]->DeleteObject();
    Input::CustomDevices.Clear();
}
