#ifndef EDITOR_HPP
#define EDITOR_HPP

#include <open_engine.hpp>

#include "open_engine/fastgltf.hpp"
#include "open_engine/logging.hpp"
#include "open_engine/ref_scope.hpp"
#include "open_engine/renderer/model3d.hpp"
#include "open_engine/renderer/renderer3d.hpp"
#include "open_engine/scene/components.hpp"
#include "panels/content_browser.hpp"
#include "panels/scene_hierarchy.hpp"

#include <Jolt/Physics/Collision/Shape/Shape.h>
#include <Jolt/Math/Real.h>
#include <Jolt/Math/Vec3.h>
#include <Jolt/Physics/Body/BodyCreationSettings.h>
#include <glm/ext/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>
#include <glm/matrix.hpp>
#include <entt/entt.hpp>
#include <glad/glad.h>
#include <GLFW/glfw3.h>
#include <ImGuizmo.h>
#include <filesystem>
#include <cstdint>
#include <imgui.h>
#include <vector>
#include <string>

namespace OpenEngine {

    enum class PlayState
    {
        Edit = 0,
        Play = 1
    };

    class CameraController : public NativeScriptableEntity
    {
        public:
            void OnCreate()
            {
            };

            void OnUpdate()
            {
                auto delta = Time::DeltaTime();

                if (HasComponent<TransformComponent>()) {
                    auto& position = GetComponent<TransformComponent>().translation;

                    if (Input::IsKeyPressed(KeyCode::Up))
                        position.y += 5.0 * delta;
                    if (Input::IsKeyPressed(KeyCode::Down))
                        position.y -= 5.0 * delta;
                    if (Input::IsKeyPressed(KeyCode::Right))
                        position.x += 5.0 * delta;
                    if (Input::IsKeyPressed(KeyCode::Left))
                        position.x -= 5.0 * delta;
                }
            };

            void OnDestroy()
            {
            };
    };

    class EditorLayer : public Layer
    {
        public:
            EditorLayer()
                : Layer("editor_layer")
            {
            }
            ~EditorLayer() {};

            void OnAttach() override
            {
                OE_PROFILE_FUNCTION();

                FramebufferSpecification specs;
                specs.attachments = { FramebufferTextureFormat::RGBA8, FramebufferTextureFormat::RED_INTEGER, FramebufferTextureFormat::Depth };
                specs.width = 1280;
                specs.height = 720;

                framebuffer = FrameBuffer::Create(specs);

                scene = CreateRef<Scene>();

                auto command_line_args = Application::Get().GetCommandLineArgs();
                if (command_line_args.count > 1) {
                    auto scene_path = command_line_args[1];
                    SceneSerializer serializer(scene);
                    serializer.Deserialize(scene_path);
                }

                editor_camera = EditorCamera(30.0f, 1920.0f/1080.0f, 0.1f, 1000.0f);

                // TODO: Add license
                icons["play"] = Texture2D::Create("resources/textures/icons/play.png");
                icons["stop"] = Texture2D::Create("resources/textures/icons/stop.png");

                // =============================================
                Entity cube = scene->CreateEntity("cube");

                auto& tc = cube.AddComponent<TransformComponent>();

                Ref<Mesh> mesh = CreateCube(cube);

                auto& mc = cube.AddComponent<MeshComponent>(mesh);
                mc.primitive_type = PrimitiveType::Cube;

                // =============================================
                Entity quad = scene->CreateEntity("quad");
                quad.AddComponent<TransformComponent>();
                quad.GetComponents<TransformComponent>().translation = {0, 0, 0};

                Ref<Mesh> quad_mesh = CreateQuad(quad, true);

                auto& mc3 = quad.AddComponent<MeshComponent>(quad_mesh);
                mc3.primitive_type = PrimitiveType::Quad;

                // =============================================
                Entity cube2 = scene->CreateEntity("cube2");
                cube2.AddComponent<TransformComponent>();
                cube2.GetComponents<TransformComponent>().translation = {2, 0, 0};

                Ref<Mesh> mesh2 = CreateCube(cube2);

                auto& mc2 = cube2.AddComponent<MeshComponent>(mesh2);
                mc2.primitive_type = PrimitiveType::Cube;

                // ============================================================

                Entity cube3 = scene->CreateEntity("glb");
                Entity cube4 = scene->CreateEntity("glb2");
                Entity cube5 = scene->CreateEntity("glb2");
                cube3.AddComponent<TransformComponent>();
                cube4.AddComponent<TransformComponent>();
                cube5.AddComponent<TransformComponent>();

                Ref<Model3D> model = Model3D::Create("./assets/models/cubes.glb");
                Ref<Model3D> model2 = Model3D::Create("./assets/models/cube_legs.glb");
                Ref<Model3D> monkey = Model3D::Create("./assets/models/monkey.glb");

                cube3.AddComponent<ModelComponent>(model);
                cube4.AddComponent<ModelComponent>(model2);
                cube5.AddComponent<ModelComponent>(monkey);

                /*
                auto view = scene->GetRegistry().view<TagComponent>();

                for (auto& entity : view) {
                    auto tag = view->get(entity);
                    OE_DEBUG("entity: {}, tag: {}", (uint32_t)entity, tag.tag);
                }
                */

                //icons["folder"] = Texture2D::Create("resources/textures/icons/folder.png");
                /*
                for (float i = 0; i < 200; i++) {
                    for (float y = 0; y < 200; y++) {
                        Entity entity = scene->CreateEntity("entity");
                        entities.push_back(entity);
                        auto& tc = entity.AddComponents<TransformComponent>();
                        tc.translation = { i / 10, y / 10, 0.0f };
                        tc.scale = { 0.1f, 0.1f, 1.0f };
                        auto& sprite = entity.AddComponents<SpriteRendererComponent>();
                        sprite.color = { i / 100.0f, y / 100.0f, 1.0f, 1.0f };
                    }
                }
                */

                scene_hierarchy.Init(scene);
            }

            Ref<Texture2D> GetIcon(const char* name)
            {
                if (icons.contains(name))
                    return icons[name];
                return nullptr;
            }

            void OnDetach() override
            {
            }

            void OnUpdate() override
            {
                FramebufferSpecification spec = framebuffer->GetSpecification();

                if (viewport_size.x > 0.0f && viewport_size.y > 0.0f &&
                        (spec.width != viewport_size.x || spec.height != viewport_size.y)) {
                    OE_PROFILE_SCOPE("Setting up Rendering");

                    framebuffer->Resize((uint32_t)viewport_size.x, (uint32_t)viewport_size.y);

                    editor_camera.SetViewportSize(viewport_size.x, viewport_size.y);
                    scene->OnViewportResize((uint32_t)viewport_size.x, (uint32_t)viewport_size.y);
                }

                framebuffer->Bind();
                //Renderer2D::ResetStats();
                RenderCommand::SetClearColor({0.11f, 0.11f, 0.15f, 1.0f});
                RenderCommand::Clear();
                framebuffer->ClearBufferI(1, -1);

                switch (state) {
                    case PlayState::Play: {
                        scene->OnUpdateRuntime();
                        break;
                    }
                    case PlayState::Edit: {
                        if (viewport_focused)
                            editor_camera.OnUpdate();
                        scene->OnUpdateEditor(editor_camera);
                        break;
                    }
                }

                auto [mx, my] = ImGui::GetMousePos();

                mx -= viewport_bounds[0].x;
                my -= viewport_bounds[0].y;
                auto viewport_size= viewport_bounds[1] - viewport_bounds[0];
                my = viewport_size.y - my;

                int mouse_x = (int)mx;
                int mouse_y = (int)my;

                static bool clicked = false;
                // Mouse Picking
                if (Input::IsMouseButtonPressed(MouseCode::ButtonLeft)
                        && !Application::Get().GetImGuiLayer()->GetBlockEvents()
                        && mouse_x >= 0 && mouse_y >= 0
                        && mouse_x < (int)viewport_size.x
                        && mouse_y < (int)viewport_size.y
                        && !ImGuizmo::IsOver()
                        && !Input::IsKeyPressed(KeyCode::LeftAlt)) {
                    if (!clicked) {
                        int id = framebuffer->ReadPixel(1, mouse_x, mouse_y);

                        selected_entity = id == -1 ?
                            Entity() : Entity((entt::entity)id, scene.get());

                        scene_hierarchy.SetSelectedEntity(selected_entity);

                    }
                    clicked = true;
                } else {
                    clicked = false;
                    selected_entity = scene_hierarchy.GetSelectedEntity();
                }

                framebuffer->Unbind();

                scene->UpdateEntities();
            }

            bool EditorKeyBinds(KeyPressedEvent& event)
            {
                if (event.GetRepeatCount() > 0)
                    return false;
                bool control = Input::IsKeyPressed(Key::LeftControl) || Input::IsKeyPressed(Key::RightControl);
                bool shift = Input::IsKeyPressed(Key::LeftShift) || Input::IsKeyPressed(Key::RightShift);

                switch(event.GetKeyCode()) {
                    case KeyCode::S: {
                        if (control) {
                            std::string file = FileDialogs::SaveFile("useless");
                            OE_TRACE("saving to filename: {}", file);
                            SaveScene(file);
                        }
                        break;
                    }
                    case KeyCode::N: {
                        if (control) {
                            scene = CreateRef<Scene>();
                            scene->OnViewportResize((uint32_t)viewport_size.x, (uint32_t)viewport_size.y);
                            scene_hierarchy.Init(scene);
                        }
                        break;
                    }
                    case KeyCode::O: {
                        if (control) {
                            std::string file = FileDialogs::OpenFile("useless");
                            OE_DEBUG("loading scene: {}", file);
                            OpenScene(file);}
                        break;
                    }
                    case KeyCode::Q: {
                        guizmo_operation = -1;
                        break;
                    }
                    case KeyCode::W: {
                        guizmo_operation = ImGuizmo::OPERATION::TRANSLATE;
                        break;
                    }
                    case KeyCode::E: {
                        guizmo_operation = ImGuizmo::OPERATION::ROTATE;
                        break;
                    }
                    case KeyCode::R: {
                        guizmo_operation = ImGuizmo::OPERATION::SCALE;
                        break;
                    }
                    case KeyCode::T: {
                        guizmo_operation = ImGuizmo::OPERATION::UNIVERSAL;
                        break;
                    }
                    case KeyCode::Delete: {
                        scene->MarkEntityForDeletion(selected_entity);
                        scene_hierarchy.ClearSelection();
                        selected_entity = {};
                        break;
                    }
                    default:
                        break;
                }

                return true;
            };

            void OnEvent(Event& event) override
            {
                OE_PROFILE_FUNCTION();

                editor_camera.OnEvent(event);

                EventDispatcher dispatcher(event);
                dispatcher.Dispatch<KeyPressedEvent>(BIND_EVENT_FN(EditorLayer::EditorKeyBinds));
            };

            float remap(float value, float minInput, float maxInput, float minOutput, float maxOutput) {
                // 1. Normalize the input to a 0.0 - 1.0 range
                float t = (value - minInput) / (maxInput - minInput);

                // 2. Use glm::mix to interpolate between the output range
                return glm::mix(minOutput, maxOutput, t);
            };

            glm::vec3 ScreenToWorld(
                    float screenX, 
                    float screenY, 
                    float screenZ,  // depth value (0.0 = near plane, 1.0 = far plane)
                    const glm::mat4& view, 
                    const glm::mat4& projection,
                    int viewportWidth,
                    int viewportHeight)
            {
                // 1. Convert screen coordinates to normalized device coordinates (NDC)
                // Screen space: origin at top-left, Y points down
                // NDC space: origin at center, range [-1, 1] for x and y

                float x = (2.0f * screenX) / viewportWidth - 1.0f;
                float y = 1.0f - (2.0f * screenY) / viewportHeight;  // Flip Y
                float z = 2.0f * screenZ - 1.0f;  // [0,1] -> [-1,1]

                glm::vec4 clipCoords(x, y, z, 1.0f);

                // 2. Transform from clip space to view space
                glm::mat4 invProjection = glm::inverse(projection);
                glm::vec4 viewCoords = invProjection * clipCoords;

                // 3. Transform from view space to world space
                glm::mat4 invView = glm::inverse(view);
                glm::vec4 worldCoords = invView * viewCoords;

                // 4. Perspective divide
                if (worldCoords.w != 0.0f) {
                    worldCoords /= worldCoords.w;
                }

                return glm::vec3(worldCoords);
            };

            void DrawViewport()
            {
                ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2{ 0, 0 });
                ImGui::Begin("Viewport");

                bool was_focused = viewport_focused;
                viewport_focused = ImGui::IsWindowFocused(ImGuiFocusedFlags_RootWindow);
                viewport_hovered = ImGui::IsWindowHovered(ImGuiHoveredFlags_RootWindow);
                Application::Get().GetImGuiLayer()->SetBlockEvents(ImGui::IsAnyItemActive());

                if (viewport_focused && !was_focused)
                    editor_camera.ResetMousePosition();

                ImVec2 viewport_panel_size = ImGui::GetContentRegionAvail();

                viewport_size = { viewport_panel_size.x, viewport_panel_size.y };

                ImVec2 imgui_cursor_position = ImGui::GetCursorScreenPos();

                uint32_t texture_id = framebuffer->GetColorAttachmentRendererID();
                ImGui::Image((void*)(uint64_t)texture_id, ImVec2{ viewport_size.x, viewport_size.y }, ImVec2{ 0, 1 }, ImVec2{ 1, 0 });

                if (ImGui::BeginDragDropTarget()) {
                   if (const ImGuiPayload* payload = ImGui::AcceptDragDropPayload("CONTENT_BROWSER_PAYLOAD")) {
                       std::filesystem::path path = (const char*)payload->Data;
                       if (path.extension() == ".oes")
                           OpenScene(path);
                   }
                    ImGui::EndDragDropTarget();
                }

                viewport_bounds[0] = { imgui_cursor_position.x, imgui_cursor_position.y };
                viewport_bounds[1] = { imgui_cursor_position.x + viewport_size.x, 
                        imgui_cursor_position.y + viewport_size.y };

                DrawGuizmos();

                ImGui::End();
                ImGui::PopStyleVar();
            };

            void DrawStats()
            {
                //auto stats = Renderer2D::GetStats();
                static float time = 0;

                time += Time::DeltaTime();

                if (time >= 1) {
                    time = 0;
                }

                ImGui::Begin("Statistics");
                ImGui::SeparatorText("Performance");
                ImGui::Text("FPS: %0.0f", 1 / Time::DeltaTime());
                //ImGui::Text("Renderer2D:");
                //ImGui::Text("\t\tDraw calls: %d", stats.draw_calls);
                //ImGui::Text("\t\tQuad count: %d", stats.quad_count);
                //ImGui::Text("\t\tVertices count: %d", stats.GetTotalVertexCount());
                //ImGui::Text("\t\tIndices count: %d", stats.GetTotalIndexCount());
                //if (selected_entity) {
                //    ImGui::SeparatorText("Entities");
                //    ImGui::Text("Selected entity:");
                //    ImGui::Text("\t\tname: %s", selected_entity.GetComponents<TagComponent>().tag.c_str());
                //    ImGui::Text("\t\tid: %d", (uint32_t)selected_entity);
                //}
                ImGui::End();
            };

            void DrawGuizmos()
            {
                Entity selected_entity = scene_hierarchy.GetSelectedEntity();
                if (selected_entity && selected_entity.HasComponent<TransformComponent>() && guizmo_operation != -1) {
                    ImGuizmo::SetOrthographic(false);
                    ImGuizmo::SetDrawlist();
                    ImGuizmo::Enable(!editor_camera.GetMoving());

                    ImGuizmo::SetRect(viewport_bounds[0].x, viewport_bounds[0].y, 
                            viewport_bounds[1].x - viewport_bounds[0].x, 
                            viewport_bounds[1].y - viewport_bounds[0].y);


                    const glm::mat4& camera_projection = editor_camera.GetProjection();
                    glm::mat4 camera_view = editor_camera.GetViewMatrix();

                    auto& transform_comp = selected_entity.GetComponents<TransformComponent>();
                    glm::mat4 transform = GetTransformFromComp(transform_comp);

                    bool snap = Input::IsKeyPressed(KeyCode::LeftControl);
                    float snap_value = 0.1f;
                    if (guizmo_operation == ImGuizmo::OPERATION::ROTATE)
                        snap_value = 10.0f;

                    float snap_values[] = { snap_value, snap_value, snap_value };

                    ImGuizmo::Manipulate(glm::value_ptr(camera_view), glm::value_ptr(camera_projection),
                            (ImGuizmo::OPERATION)guizmo_operation, ImGuizmo::LOCAL,
                            glm::value_ptr(transform), nullptr,
                            snap ? snap_values : nullptr);

                    if (ImGuizmo::IsUsing()) {
                        glm::vec3 translation, rotation, scale;
                        Math::DecomposeTransform(transform, translation, rotation, scale);

                        glm::vec3 delta_rotation = rotation - transform_comp.rotation;
                        transform_comp.translation = translation;
                        transform_comp.rotation += delta_rotation;
                        transform_comp.scale = scale;
                    }
                } else
                    guizmo_operation = -1;
            };

            void OpenScene(const std::string& file)
            {
                if (!file.empty()) {
                    scene = CreateRef<Scene>();
                    scene->OnViewportResize((uint32_t)viewport_size.x, (uint32_t)viewport_size.y);
                    scene_hierarchy.Init(scene);

                    SceneSerializer serializer(scene);
                    serializer.Deserialize(file);
                }
            }

            void SaveScene(const std::string& file)
            {
                if (!file.empty()) {
                    SceneSerializer serializer(scene);
                    serializer.Serialize(file);
                }
            }

            void DrawPlayBar()
            {
                ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, { 0, 2 });
                ImGui::PushStyleVar(ImGuiStyleVar_ItemInnerSpacing, { 0, 0 });
                ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, { 0, 0 });
                ImGui::PushStyleColor(ImGuiCol_Button, { 0, 0, 0, 0 });
                ImGui::PushStyleColor(ImGuiCol_ButtonHovered, { 0.200f, 0.207f, 0.286f, 1.0f });
                ImGui::PushStyleColor(ImGuiCol_ButtonActive, { 0, 0, 0, 0 });

                ImGui::Begin("##play_state_bar", nullptr, ImGuiWindowFlags_NoDecoration
                        | ImGuiWindowFlags_NoScrollWithMouse
                        | ImGuiWindowFlags_NoScrollbar
                        | ImGuiWindowFlags_NoTitleBar);

                float size = ImGui::GetContentRegionAvail().y;

                auto icon = state == PlayState::Play ? icons["stop"] : icons["play"];

                ImGui::SetCursorPosX((ImGui::GetContentRegionAvail().x * 0.5) - (size * 0.5) );
                if (ImGui::ImageButton("##play_button",
                        (ImTextureID)icon->GetID(), { size, size })) {
                    switch (state) {
                        case PlayState::Play : {
                            OnSceneStop();
                            break;
                        }
                        case PlayState::Edit : {
                            OnScenePlay();
                            break;
                        }
                    }
                }

                ImGui::End();

                ImGui::PopStyleColor(3);
                ImGui::PopStyleVar(3);
            }

            void OnImGuiRender() override
            {
                OE_PROFILE_FUNCTION();

                ImGui::PushStyleVar(ImGuiStyleVar_WindowMinSize, ImVec2(450, 35));
                {
                    if (ImGui::BeginMainMenuBar()) {
                        if (ImGui::BeginMenu("File")) {
                            if (ImGui::MenuItem("Save Scene As", "Ctrl+S")) {
                                std::string file = FileDialogs::SaveFile("useless");
                                OE_TRACE("saving to filename: {}", file);
                                SaveScene(file);
                            }
                            if (ImGui::MenuItem("Open Scene", "Ctrl+O")) {
                                std::string file = FileDialogs::OpenFile("useless");
                                OE_DEBUG("loading scene: {}", file);
                                OpenScene(file);
                            }
                            if (ImGui::MenuItem("New Scene", "Ctrl+N")) {
                                scene = CreateRef<Scene>();
                                scene->OnViewportResize((uint32_t)viewport_size.x, (uint32_t)viewport_size.y);
                                scene_hierarchy.Init(scene);
                            }
                            ImGui::Separator();
                            if (ImGui::MenuItem("Exit"))
                                Application::Get().Close();
                            ImGui::EndMenu();
                        }
                        ImGui::EndMainMenuBar();
                    }
                }

                ImGui::DockSpaceOverViewport();

                DrawStats();
                scene_hierarchy.OnImGuiRender();
                browser.OnImGuiRender();
                DrawViewport();
                DrawPlayBar();

                ImGui::ShowDemoWindow();
                ImGui::PopStyleVar();
            };

            void OnScenePlay()
            {
                state = PlayState::Play;
                scene->OnRuntimeStart();
            }
            void OnSceneStop()
            {
                state = PlayState::Edit;
                scene->OnRuntimeStop();
            }

        private:
            ContentBrowserPanel browser;
            std::vector<Time::ProfilingResult> profiling_results;

            glm::vec2 world_pos_cursor = { 0.0f, 0.0f };

            glm::vec2 viewport_size = { 0.0f, 0.0f };
            Ref<OpenEngine::FrameBuffer> framebuffer;
            glm::vec2 viewport_bounds[2];

            bool viewport_focused = false, viewport_hovered = false;

            SceneHierarchy scene_hierarchy;

            EditorCamera editor_camera;

            int guizmo_operation = -1;

            // TODO: Rename this to GameState
            PlayState state = PlayState::Edit;

            std::unordered_map<const char*, Ref<Texture2D>> icons;
            Ref<Scene> scene;
            Entity selected_entity;

            std::vector<Entity> entities;
    };
}

class EditorApp : public OpenEngine::Application
{
    public:
        EditorApp(OpenEngine::ApplicationCommandLineArgs args);
        ~EditorApp();
};

#endif // EDITOR_HPP
