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

#include "Engine/Platform/Window.h"
#include "Engine/Engine/Engine.h"
#include "Engine/Graphics/RenderTask.h"
#include "Engine/Platform/WindowsManager.h"
#include "Engine/Graphics/GPUSwapChain.h"
#include "Engine/Graphics/GPUDevice.h"
#include "Engine/Input/Input.h"
#include "Engine/Platform/IGuiData.h"
#include "Engine/Scripting/ScriptingType.h"
#include "Engine/Profiler/ProfilerCPU.h"
#include "Engine/Scripting/ManagedCLR/MException.h"
#include "Engine/Scripting/ManagedCLR/MUtils.h"
#include "Engine/Scripting/ManagedCLR/MMethod.h"
#include "Engine/Scripting/ManagedCLR/MClass.h"

#if USE_CSHARP
// Helper macros for calling C# events
#define BEGIN_INVOKE_EVENT(name, paramsCount) \
	auto managedInstance = GetManagedInstance(); \
    if (managedInstance) \
	{ \
	    static MMethod* _method_##name = nullptr; \
	    if (_method_##name == nullptr) \
	    { \
		    _method_##name = GetClass()->GetMethod("Internal_" #name, paramsCount); \
		    if (_method_##name == nullptr) \
		    { \
			    LOG(Fatal, "Missing Window method " #name); \
		    } \
	    }
#define END_INVOKE_EVENT(name) \
        if (exception) \
	    { \
		    MException ex(exception); \
		    ex.Log(LogType::Error, TEXT("Window." #name)); \
	    } \
	}
#define INVOKE_EVENT(name, paramsCount, param0, param1, param2) BEGIN_INVOKE_EVENT(name, paramsCount) \
	    void* params[3]; \
	    params[0] = param0; \
	    params[1] = param1; \
	    params[2] = param2; \
	    MObject* exception = nullptr; \
	    _method_##name->Invoke(managedInstance, params, &exception); \
	    END_INVOKE_EVENT(name)
#define INVOKE_EVENT_PARAMS_3(name, param0, param1, param2) INVOKE_EVENT(name, 3, param0, param1, param2)
#define INVOKE_EVENT_PARAMS_2(name, param0, param1) INVOKE_EVENT(name, 2, param0, param1, nullptr)
#define INVOKE_EVENT_PARAMS_1(name, param0) INVOKE_EVENT(name, 1, param0, nullptr, nullptr)
#define INVOKE_EVENT_PARAMS_0(name) INVOKE_EVENT(name, 0, nullptr, nullptr, nullptr)
#define INVOKE_DRAG_EVENT(name) \
	if (result != DragDropEffect::None) return; \
    BEGIN_INVOKE_EVENT(name, 3) \
	void* params[3]; \
	params[0] = (void*)&mousePosition; \
	Array<String> outputData; \
	bool isText; \
	if(data->GetType() == IGuiData::Type::Text)\
	{ \
		isText = true; outputData.Add(data->GetAsText()); \
	} \
	else \
	{ \
		isText = false; data->GetAsFiles(&outputData); \
	} \
	params[1] = (void*)&isText; \
	MArray* outputDataManaged = MCore::Array::New(MCore::TypeCache::String, outputData.Count()); \
    MString** outputDataManagedPtr = MCore::Array::GetAddress<MString*>(outputDataManaged); \
	for (int32 i = 0; i < outputData.Count(); i++) \
        outputDataManagedPtr[i] = MUtils::ToString(outputData[i]); \
	params[2] = outputDataManaged; \
	MObject* exception = nullptr; \
	auto resultObj = _method_##name->Invoke(GetManagedInstance(), params, &exception); \
    if (resultObj) \
	    result = (DragDropEffect)MUtils::Unbox<int32>(resultObj); \
	END_INVOKE_EVENT(name)
#else
#define INVOKE_EVENT(name, paramsCount, param0, param1, param2)
#define INVOKE_EVENT_PARAMS_3(name, param0, param1, param2) INVOKE_EVENT(name, 3, param0, param1, param2)
#define INVOKE_EVENT_PARAMS_2(name, param0, param1) INVOKE_EVENT(name, 2, param0, param1, nullptr)
#define INVOKE_EVENT_PARAMS_1(name, param0) INVOKE_EVENT(name, 1, param0, nullptr, nullptr)
#define INVOKE_EVENT_PARAMS_0(name) INVOKE_EVENT(name, 0, nullptr, nullptr, nullptr)
#define INVOKE_DRAG_EVENT(name)
#endif

WindowBase::WindowBase(const CreateWindowSettings& settings)
    : ScriptingObject(SpawnParams(Guid::New(), TypeInitializer))
    , _visible(false)
    , _minimized(false)
    , _maximized(false)
    , _isClosing(false)
    , _showAfterFirstPaint(settings.ShowAfterFirstPaint)
    , _focused(false)
    , _swapChain(nullptr)
    , _settings(settings)
    , _title(settings.Title)
    , _cursor(CursorType::Default)
    , _clientSize(settings.Size)
    , _dpi(96)
    , _dpiScale(1.0f)
    , _trackingMouseOffset(Float2::Zero)
    , RenderTask(nullptr)
{
    // Update window location based on start location
    if (settings.StartPosition == WindowStartPosition::CenterParent
        || settings.StartPosition == WindowStartPosition::CenterScreen)
    {
        Rectangle parentBounds = Rectangle(Float2::Zero, Platform::GetDesktopSize());
        if (settings.Parent != nullptr && settings.StartPosition == WindowStartPosition::CenterParent)
            parentBounds = settings.Parent->GetClientBounds();

        // Move to the center of target bounds area
        // A little hack but platform implementation constructor will place a window
        ((CreateWindowSettings&)settings).Position = parentBounds.Location + (parentBounds.Size - settings.Size) * 0.5f;
    }

    WindowsManager::Register((Window*)this);
}

WindowBase::~WindowBase()
{
    ASSERT(!RenderTask);
    ASSERT(!_swapChain);
    WindowsManager::Unregister((Window*)this);
}

bool WindowBase::IsMain() const
{
    const auto window = Engine::MainWindow;
    return window == this || window == nullptr;
}

bool WindowBase::IsFullscreen() const
{
    return _swapChain ? _swapChain->IsFullscreen() : false;
}

void WindowBase::SetIsFullscreen(bool isFullscreen)
{
    LOG(Info, "Changing window fullscreen mode to {0}", isFullscreen);

    if (_swapChain)
    {
        _swapChain->SetFullscreen(isFullscreen);
    }
}

bool WindowBase::IsVisible() const
{
    return _visible;
}

void WindowBase::SetIsVisible(bool isVisible)
{
    // Check if state will change
    if (IsVisible() != isVisible)
    {
        // Check if show or hide a window
        if (isVisible)
            Show();
        else
            Hide();
    }
}

String WindowBase::ToString() const
{
    return GetTitle();
}

void WindowBase::OnDeleteObject()
{
#if !USE_EDITOR

    // Unlink main task (if was used)
    if (RenderTask && RenderTask == MainRenderTask::Instance)
    {
        MainRenderTask::Instance->SwapChain = nullptr;
        RenderTask = nullptr;
    }

#endif

    // Release resources
    SAFE_DELETE(RenderTask);
    SAFE_DELETE(_swapChain);

    // Base
    ScriptingObject::OnDeleteObject();
}

bool WindowBase::GetRenderingEnabled() const
{
    return RenderTask && RenderTask->Enabled;
}

void WindowBase::SetRenderingEnabled(bool value)
{
    if (RenderTask)
        RenderTask->Enabled = value;
}

void WindowBase::OnCharInput(Char c)
{
    PROFILE_CPU_NAMED("GUI.OnCharInput");
    CharInput(c);
    INVOKE_EVENT_PARAMS_1(OnCharInput, &c);
}

void WindowBase::OnKeyDown(KeyboardKeys key)
{
    PROFILE_CPU_NAMED("GUI.OnKeyDown");
    KeyDown(key);
    INVOKE_EVENT_PARAMS_1(OnKeyDown, &key);
}

void WindowBase::OnKeyUp(KeyboardKeys key)
{
    PROFILE_CPU_NAMED("GUI.OnKeyUp");
    KeyUp(key);
    INVOKE_EVENT_PARAMS_1(OnKeyUp, &key);
}

void WindowBase::OnMouseDown(const Float2& mousePosition, MouseButton button)
{
    PROFILE_CPU_NAMED("GUI.OnMouseDown");
    MouseDown(mousePosition, button);
    INVOKE_EVENT_PARAMS_2(OnMouseDown, (void*)&mousePosition, &button);
}

void WindowBase::OnMouseUp(const Float2& mousePosition, MouseButton button)
{
    PROFILE_CPU_NAMED("GUI.OnMouseUp");
    MouseUp(mousePosition, button);
    INVOKE_EVENT_PARAMS_2(OnMouseUp, (void*)&mousePosition, &button);
}

void WindowBase::OnMouseDoubleClick(const Float2& mousePosition, MouseButton button)
{
    PROFILE_CPU_NAMED("GUI.OnMouseDoubleClick");
    MouseDoubleClick(mousePosition, button);
    INVOKE_EVENT_PARAMS_2(OnMouseDoubleClick, (void*)&mousePosition, &button);
}

void WindowBase::OnMouseWheel(const Float2& mousePosition, float delta)
{
    PROFILE_CPU_NAMED("GUI.OnMouseWheel");
    MouseWheel(mousePosition, delta);
    INVOKE_EVENT_PARAMS_2(OnMouseWheel, (void*)&mousePosition, &delta);
}

void WindowBase::OnMouseMove(const Float2& mousePosition)
{
    PROFILE_CPU_NAMED("GUI.OnMouseMove");
    MouseMove(mousePosition);
    INVOKE_EVENT_PARAMS_1(OnMouseMove, (void*)&mousePosition);
}

void WindowBase::OnMouseLeave()
{
    PROFILE_CPU_NAMED("GUI.OnMouseLeave");
    MouseLeave();
    INVOKE_EVENT_PARAMS_0(OnMouseLeave);
}

void WindowBase::OnTouchDown(const Float2& pointerPosition, int32 pointerId)
{
    PROFILE_CPU_NAMED("GUI.OnTouchDown");
    TouchDown(pointerPosition, pointerId);
    INVOKE_EVENT_PARAMS_2(OnTouchDown, (void*)&pointerPosition, &pointerId);
}

void WindowBase::OnTouchMove(const Float2& pointerPosition, int32 pointerId)
{
    PROFILE_CPU_NAMED("GUI.OnTouchMove");
    TouchMove(pointerPosition, pointerId);
    INVOKE_EVENT_PARAMS_2(OnTouchMove, (void*)&pointerPosition, &pointerId);
}

void WindowBase::OnTouchUp(const Float2& pointerPosition, int32 pointerId)
{
    PROFILE_CPU_NAMED("GUI.OnTouchUp");
    TouchUp(pointerPosition, pointerId);
    INVOKE_EVENT_PARAMS_2(OnTouchUp, (void*)&pointerPosition, &pointerId);
}

void WindowBase::OnDragEnter(IGuiData* data, const Float2& mousePosition, DragDropEffect& result)
{
    DragEnter(data, mousePosition, result);
    INVOKE_DRAG_EVENT(OnDragEnter);
}

void WindowBase::OnDragOver(IGuiData* data, const Float2& mousePosition, DragDropEffect& result)
{
    DragOver(data, mousePosition, result);
    INVOKE_DRAG_EVENT(OnDragOver);
}

void WindowBase::OnDragDrop(IGuiData* data, const Float2& mousePosition, DragDropEffect& result)
{
    DragDrop(data, mousePosition, result);
    INVOKE_DRAG_EVENT(OnDragDrop);
}

void WindowBase::OnDragLeave()
{
    INVOKE_EVENT_PARAMS_0(OnDragLeave);
}

void WindowBase::OnHitTest(const Float2& mousePosition, WindowHitCodes& result, bool& handled)
{
    HitTest(mousePosition, result, handled);
    if (handled)
        return;
    INVOKE_EVENT_PARAMS_3(OnHitTest, (void*)&mousePosition, &result, &handled);
}

void WindowBase::OnLeftButtonHit(WindowHitCodes hit, bool& result)
{
    LeftButtonHit(hit, result);
    if (result)
        return;
    INVOKE_EVENT_PARAMS_2(OnLeftButtonHit, &hit, &result);
}

void WindowBase::OnClosing(ClosingReason reason, bool& cancel)
{
    Closing(reason, cancel);
    INVOKE_EVENT_PARAMS_2(OnClosing, &reason, &cancel);
}

StringView WindowBase::GetInputText() const
{
    return _settings.AllowInput && _focused ? Input::GetInputText() : StringView::Empty;
}

bool WindowBase::GetKey(KeyboardKeys key) const
{
    return _settings.AllowInput && _focused ? Input::GetKey(key) : false;
}

bool WindowBase::GetKeyDown(KeyboardKeys key) const
{
    return _settings.AllowInput && _focused ? Input::GetKeyDown(key) : false;
}

bool WindowBase::GetKeyUp(KeyboardKeys key) const
{
    return _settings.AllowInput && _focused ? Input::GetKeyUp(key) : false;
}

Float2 WindowBase::GetMousePosition() const
{
    return _settings.AllowInput && _focused ? ScreenToClient(Input::GetMouseScreenPosition()) : Float2::Minimum;
}

void WindowBase::SetMousePosition(const Float2& position) const
{
    if (_settings.AllowInput && _focused)
    {
        Input::SetMouseScreenPosition(ClientToScreen(position));
    }
}

Float2 WindowBase::GetMousePositionDelta() const
{
    return _settings.AllowInput && _focused ? Input::GetMousePositionDelta() : Float2::Zero;
}

float WindowBase::GetMouseScrollDelta() const
{
    return _settings.AllowInput && _focused ? Input::GetMouseScrollDelta() : 0.0f;
}

bool WindowBase::GetMouseButton(MouseButton button) const
{
    return _settings.AllowInput && _focused ? Input::GetMouseButton(button) : false;
}

bool WindowBase::GetMouseButtonDown(MouseButton button) const
{
    return _settings.AllowInput && _focused ? Input::GetMouseButtonDown(button) : false;
}

bool WindowBase::GetMouseButtonUp(MouseButton button) const
{
    return _settings.AllowInput && _focused ? Input::GetMouseButtonUp(button) : false;
}

void WindowBase::OnShow()
{
    PROFILE_CPU_NAMED("GUI.OnShow");
    INVOKE_EVENT_PARAMS_0(OnShow);
    Shown();
}

void WindowBase::OnResize(int32 width, int32 height)
{
    PROFILE_CPU_NAMED("GUI.OnResize");
    if (_swapChain)
        _swapChain->Resize(width, height);
    if (RenderTask)
        RenderTask->Resize(width, height);
    Resized({ static_cast<float>(width), static_cast<float>(height) });
    INVOKE_EVENT_PARAMS_2(OnResize, &width, &height);
}

void WindowBase::OnClosed()
{
    // We have to call this from close event to finish WindowBase destroy process (you cannot use WindowBase after Close)
    ASSERT(_isClosing);

    // Dispose swap chain (it will wait for GPU work to be done)
    if (_swapChain)
        _swapChain->ReleaseGPU();

    // Send event
    Closed();
    INVOKE_EVENT_PARAMS_0(OnClosed);

    // Unregister
    WindowsManager::Unregister(static_cast<Window*>(this));

    // Disable rendering
    if (RenderTask)
        RenderTask->Enabled = false;

    // Delete object
    DeleteObject(1);
}

void WindowBase::OnGotFocus()
{
    if (_focused)
        return;
    _focused = true;

    GotFocus();
    INVOKE_EVENT_PARAMS_0(OnGotFocus);
}

void WindowBase::OnLostFocus()
{
    if (!_focused)
        return;
    _focused = false;

    LostFocus();
    INVOKE_EVENT_PARAMS_0(OnLostFocus);
}

void WindowBase::OnUpdate(float dt)
{
    PROFILE_CPU_NAMED("GUI.OnUpdate");
    Update(dt);
    INVOKE_EVENT_PARAMS_1(OnUpdate, &dt);
}

void WindowBase::OnDraw()
{
    PROFILE_CPU_NAMED("GUI.OnDraw");
    INVOKE_EVENT_PARAMS_0(OnDraw);
    Draw();
}

bool WindowBase::InitSwapChain()
{
    // Setup swapchain
    if (_swapChain == nullptr)
    {
        _swapChain = GPUDevice::Instance->CreateSwapChain((Window*)this);
        if (!_swapChain)
            return true;
    }
    if (_swapChain->Resize(static_cast<int32>(_clientSize.X), static_cast<int32>(_clientSize.Y)))
        return true;
    if (_settings.Fullscreen)
        _swapChain->SetFullscreen(true);

    // Setup render task
    if (RenderTask == nullptr)
    {
#if !USE_EDITOR
        if (IsMain())
        {
            // Override main task output (render directly to the window backbuffer)
            ASSERT(MainRenderTask::Instance);
            ASSERT(MainRenderTask::Instance->SwapChain == nullptr);
            RenderTask = MainRenderTask::Instance;
            MainRenderTask::Instance->Deleted.Bind<WindowBase, &WindowBase::OnMainRenderTaskDelete>(this);
        }
        else
#endif
        {
            RenderTask = New<::RenderTask>();
        }
        RenderTask->SwapChain = _swapChain;
        RenderTask->Enabled = false;
        RenderTask->Order = 100;
    }

    return false;
}

void WindowBase::Show()
{
    const auto clientSize = GetClientSize();
    const auto width = static_cast<int32>(clientSize.X);
    const auto height = static_cast<int32>(clientSize.Y);
    _visible = true;

    // Ensure to have backbuffer and swapchain ready
    if (InitSwapChain())
    {
        Platform::Fatal(TEXT("Cannot init rendering output for a window."));
    }

    if (RenderTask)
    {
        // Resize render task to fit WindowBase client size
        RenderTask->Resize(width, height);

        // Enable rendering
        RenderTask->Enabled = true;
    }

    // Call GUI event
    OnResize(width, height);
    OnShow();
}

void WindowBase::Hide()
{
    if (!_visible)
        return;
    EndClippingCursor();
    EndTrackingMouse();
    _visible = false;
    _showAfterFirstPaint = _settings.ShowAfterFirstPaint;
    Hidden();
}

void WindowBase::Close(ClosingReason reason)
{
    // Prevent from calling close during or after close action
    if (_isClosing)
        return;
    _isClosing = true;

    // Check if can close window
    bool cancel = false;
    OnClosing(reason, cancel);
    if (cancel && reason != ClosingReason::EngineExit) // Disallow to skip closing on Engine Exit (danger situation)
    {
        // Back
        _isClosing = false;
        return;
    }

    // Close
    Hide();
    OnClosed();
}

bool WindowBase::IsForegroundWindow() const
{
    return _focused;
}
