#ifndef SHMUP_HPP
#define SHMUP_HPP

#include <GL/gl.h>
#include <open_engine.hpp>

#include <fstream>
#include <dlfcn.h>
#include <glm/ext/matrix_transform.hpp>
#include <glm/glm.hpp>
#include <random>
#include <string>
#include <unistd.h>
#include <vector>

enum class GameState
{
    Playing,
    Paused,
    GameOver
};

class Shmup : public OpenEngine::Layer
{
    public:
        Shmup()
            : camera((float)OpenEngine::Application::Get().GetWindow().GetWidth() /
                    OpenEngine::Application::Get().GetWindow().GetHeight(), 1.0f)
        {
        }
        ~Shmup() {};

        void OnAttach() override
        {
            OE_PROFILE_FUNCTION();
            {
                OE_PROFILE_SCOPE("Texture2D Creation");
                ship = OpenEngine::Texture2D::Create("assets/textures/shmup/ship.png");
                enemy_ship = OpenEngine::Texture2D::Create("assets/textures/shmup/enemy_ship.png");
                bullet_tx = OpenEngine::Texture2D::Create("assets/textures/shmup/bullet.png");
            }

            LoadMap();
        }

        void OnDetach() override
        {
        }

        struct Bounds {
            glm::vec2 min; // Bottom-Left
            glm::vec2 max; // Top-Right
        };

        Bounds GetBounds(glm::vec2 position, glm::vec2 scale) {
            Bounds b;
            // Because the original vertices are -0.5 to 0.5, 
            // the distance from center to edge is exactly scale * 0.5
            glm::vec2 halfScale = scale * 0.5f;

            b.min = position - halfScale;
            b.max = position + halfScale;
            return b;
        }

        bool CheckCollision(const Bounds& a, const Bounds& b) {
            bool overlapX = a.max.x >= b.min.x && b.max.x >= a.min.x;
            bool overlapY = a.max.y >= b.min.y && b.max.y >= a.min.y;

            return overlapX && overlapY;
        }

        void MovePlayerShip()
        {
            double delta = OpenEngine::Time::DeltaTime();
            if (OpenEngine::Input::IsKeyPressed(OpenEngine::KeyCode::Up)) {
                tr1.position.y += 1 * delta;
            }
            if (OpenEngine::Input::IsKeyPressed(OpenEngine::KeyCode::Down)) {
                tr1.position.y -= 1 * delta;
            }
            if (OpenEngine::Input::IsKeyPressed(OpenEngine::KeyCode::Right)) {
                tr1.position.x += 1 * delta;
            }
            if (OpenEngine::Input::IsKeyPressed(OpenEngine::KeyCode::Left)) {
                tr1.position.x -= 1 * delta;
            }

            if (tr1.position.y <= -0.9)
                tr1.position.y = -0.9;
            if (tr1.position.y >= -0.3)
                tr1.position.y = -0.3;
            if (tr1.position.x <= -0.8)
                tr1.position.x = -0.8;
            if (tr1.position.x >= 0.8)
                tr1.position.x = 0.8;
        }

        bool MovePlayerShipDiscrete(OpenEngine::KeyPressedEvent& event)
        {
            if (state == GameState::Playing) {
                double delta = OpenEngine::Time::DeltaTime();
                if (event.GetKeyCode() == OpenEngine::KeyCode::Right) {
                    tr1.position.x += 0.1f;
                    return true;
                }
                if (event.GetKeyCode() == OpenEngine::KeyCode::Left) {
                    tr1.position.x -= 0.1f;
                    return true;
                }

                if (tr1.position.y <= -0.9)
                    tr1.position.y = -0.9;
                if (tr1.position.y >= -0.3)
                    tr1.position.y = -0.3;
                if (tr1.position.x <= -0.8)
                    tr1.position.x = -0.8;
                if (tr1.position.x >= 0.8)
                    tr1.position.x = 0.8;
            }

            return false;
        }

        void MoveEntities(double time_step) {
            double delta = time_step;

            // Updating enemy positions
            for (auto& enemy : enemies) {
                enemy.position.y -= 0.3 * delta;
                if (state == GameState::Playing && (enemy.position.y <= -1.1 || CheckCollision(
                            GetBounds(enemy.position, enemy.size),
                            GetBounds(tr1.position, tr1.size))))
                    lives--;
            }

            // Updating friendly bullets positions
            for (auto& bullet : bullets) {
                bullet.position.y += 1.1 * delta;
            }
        }

        void UpdateEntity()
        {
            double delta = OpenEngine::Time::DeltaTime();

            MoveEntities(delta);

            // Deletes enemies offscreen
            std::erase_if(enemies, [](const auto& enemy) {
                    return enemy.position.y <= -1.1f;
                    });
            // Deletes bullets offscreen
            std::erase_if(bullets, [](const auto& bullet) {
                    return bullet.position.y >= 1.2f;
                    });

            // Lowers lives upon collision between player and enemy
            for (auto& enemy : enemies) {
                if (state == GameState::Playing) {
                    if (enemy.position.y <= -1.1)
                        lives--;

                    if (CheckCollision(
                                GetBounds(enemy.position, enemy.size),
                                GetBounds(tr1.position, tr1.size)))
                        lives = 0;
                }
            }

            // Deletes enemy and bullet and increases score after collision
            for (auto& bullet : bullets) {
                for (auto& enemy : enemies) {
                    Bounds bullet_bounds = GetBounds(bullet.position, bullet.size);
                    Bounds enemy_bounds = GetBounds(enemy.position, enemy.size);

                    if (CheckCollision(bullet_bounds, enemy_bounds)) {
                        std::erase_if(bullets, [&](const OpenEngine::Transform& _bullet) {
                                return _bullet.position == bullet.position;
                                });
                        std::erase_if(enemies, [&](const auto& _enemy) {
                                return _enemy.position == enemy.position;
                                });
                        score++;
                    }
                }
            }

        }

        void Render()
        {
            OE_PROFILE_FUNCTION()
            {
                OE_PROFILE_SCOPE("Setting up Rendering");
                OpenEngine::RenderCommand::SetClearColor({0.11f, 0.11f, 0.15f, 1.0f});
                //OpenEngine::RenderCommand::SetClearColor({1.0f, 0.11f, 0.15f, 1.0f});
                OpenEngine::RenderCommand::Clear();
                OpenEngine::Renderer2D::BeginScene(camera.GetCamera());
            }
            {
                OE_PROFILE_SCOPE("Drawing Quads");
                OpenEngine::Renderer2D::DrawQuad(tr1, ship, -1);

                for (auto& enemy : enemies)
                    OpenEngine::Renderer2D::DrawQuad(enemy, enemy_ship, -1);
                for (auto& bullet : bullets)
                    OpenEngine::Renderer2D::DrawQuad(bullet, bullet_tx, -1);
            }

            OpenEngine::Renderer2D::EndScene();

        }

        void OnUpdate() override
        {
            float delta = OpenEngine::Time::DeltaTime();

            Render();

            UpdateEntity();

            if (state == GameState::Playing) {
                if (spawn_cooldown >= spawn_min_time) {
                    //SpawnEnemyRandom();
                    ReadMap();
                    spawn_cooldown = 0;
                    if (spawn_min_time > 0.5)
                        spawn_min_time -= 0.01;
                }

                //MovePlayerShip();

                if (shooting)
                    SpawnBullet();

                shot_cooldown += delta;
                spawn_cooldown += delta;
            }

            if (lives <= 0)
                state = GameState::GameOver;
        }

        void Reset()
        {
            tr1 = {glm::vec3(0.0f, -0.9f, -0.1f), glm::vec3(0.1f), 0.0f};

            score = 0;
            lives = 5;

            bullets.clear();
            enemies.clear();

            state = GameState::Playing;
        }

        void SpawnEnemyRandom()
        {
            static std::uniform_int_distribution<int> dist{-8, 8};

            SpawnEnemy(dist(rd));
        };

        void SpawnEnemy(int x)
        {
            OpenEngine::Transform enemy_tr({{x/10.0f, 1.2f, 0.1f}, glm::vec3(0.1f), 180.0f});
            enemies.emplace_back(enemy_tr);
        };

        void LoadMap(const char* path = "assets/maps/lvl1.txt")
        {
            std::string buffer;

            std::ifstream map_file(path);

            while (std::getline(map_file, buffer)) {
                map.emplace_back(buffer);
            }
        };

        void ReadMap()
        {
            static int line_n = 0;
            if (line_n >= map.size())
                return;

            auto line = map[line_n];

            int position = -8;
            for (int i = 0; i < line.length(); i++) {
                if (line[i] == '#')
                    SpawnEnemy(position);
                position++;
            }

            line_n++;
        };

        void SpawnBullet()
        {
            if (shot_cooldown >= 0.2) {
                OpenEngine::Transform bullet_tr;
                bullet_tr.size = glm::vec3(0.03f);
                bullet_tr.position.x = tr1.position.x;
                bullet_tr.position.y = tr1.position.y + 0.1;

                bullets.emplace_back(bullet_tr);

                shot_cooldown = 0;
            }
        }

        bool ProcessKeyPressedEvents(OpenEngine::KeyPressedEvent& event)
        {
            if (event.GetKeyCode() == OpenEngine::KeyCode::Space) {
                shooting = true;
                return true;
            }
            if (state == GameState::GameOver && event.GetKeyCode() == OpenEngine::KeyCode::Enter) {
                Reset();
                return true;
            }

            return false;
        };

        bool ProcessKeyReleased(OpenEngine::KeyReleasedEvent& event)
        {
            if (event.GetKeyCode() == OpenEngine::KeyCode::Space)
                shooting = false;

            return false;
        }

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

            OpenEngine::EventDispatcher dispatcher(event);

            dispatcher.Dispatch<OpenEngine::KeyPressedEvent>(BIND_EVENT_FN(Shmup::MovePlayerShipDiscrete));
            dispatcher.Dispatch<OpenEngine::KeyPressedEvent>(BIND_EVENT_FN(Shmup::ProcessKeyPressedEvents));
            dispatcher.Dispatch<OpenEngine::KeyReleasedEvent>(BIND_EVENT_FN(Shmup::ProcessKeyReleased));
            {
                OE_PROFILE_SCOPE("Camera OnEvent");
                camera.OnEvent(event);
            }
        }

        //void OnImGuiRender() override
        //{
        //    ImGuiIO io = ImGui::GetIO();
        //    static auto m_Font = io.Fonts->AddFontFromFileTTF("/usr/share/fonts/TTF/JetBrainsMono-Regular.ttf", 120.0f);

        //    ImGui::GetForegroundDrawList()->AddText(m_Font, 48.0f, {5.0f, 5.0f}, 0xffffffff, "test");
        //}

        GameState GetGameState() { return state; };
        int GetScore() { return score; };
        int GetLives() { return lives; };

    private:
        OpenEngine::Ref<OpenEngine::Texture2D> ship;
        OpenEngine::Ref<OpenEngine::Texture2D> enemy_ship;
        OpenEngine::Ref<OpenEngine::Texture2D> bullet_tx;
        OpenEngine::Transform tr1 = {glm::vec3(0.0f, -0.9f, -0.1f), glm::vec3(0.1f), 0.0f};
        std::vector<OpenEngine::Transform> enemies, bullets;
        int score = 0;
        int lives = 5;

        bool shooting = false;
        float spawn_cooldown = 0, shot_cooldown = 0;
        float spawn_min_time = 1.0f;
        std::vector<std::string> map;

        std::random_device rd;
        std::mt19937 e{rd()}; // or std::default_random_engine e{rd()};

        OpenEngine::OrthographicCameraController camera;
        GameState state = GameState::Playing;

        //OpenEngine::Ref<Overlay> overlay;
};

#endif // SHMUP_HPP
