#pragma once

#ifndef PLAYERCAMERA_H
#define PLAYERCAMERA_H

// Includes
#include <vector>
#include <iostream>
#include <GLFW/glfw3.h>
#include <glm/glm.hpp> // GLM is a header only library for math in OpenGL
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>
#include <glm/gtc/random.hpp>
#include <cmath>
#include "Utils.h"

#define GLM_FORCE_DEPTH_ZERO_TO_ONE // This is needed for glm to work with vulkan
namespace Student
{
    // Defines several possible options for camera movement. Used as abstraction to stay away from window-system specific input methods
    enum Camera_Movement
    {
        FORWARD,
        BACKWARD,
        LEFT,
        RIGHT,
        UPWARD,
        STOP
    };

    // Default camera values
    const glm::vec3 WORLDUP(0.0f, 1.0f, 0.0f);
    const glm::vec3 UP(0.0f, 1.0f, 0.0f);
    const glm::vec3 POSITION(0.0f, 0.0f, 2.0f);
    const glm::vec3 FRONT(0.0f, 0.0f, -1.0f);
    const float YAW = -90.0f;
    const float PITCH = 0.0f;
    const float SPEED = 1.0f;
    const float ANGULARSPEED = 0.1f;
    const float FOV = 60;
    const float ASPECTRATIO = 800.0f / 600.0f;
    const float NEARPLANE = 0.1f;
    const float FARPLANE = 100.0f;

    // Variables for camera mouse control -> go into mouse callback functions so they have to be namespace global variables !!!
    float lastMouseX = 400.0;
    float lastMouseY = 300.0;
    float mouseOffsetX = 0.0;
    float mouseOffsetY = 0.0;
    bool  firstMouseMove = true;
    float angularSpeed = ANGULARSPEED;
    float yaw = -90.0;
    float pitch = 0.0;
    float fov = 60;

    // An abstract camera class that processes input and calculates the corresponding Euler Angles, Vectors and Matrices for use in OpenGL
    class AbstractCamera
    {
    public:
        // Camera Attributes
        glm::vec3 Position;
        glm::vec3 Up;
        glm::vec3 Right;
        glm::vec3 Front;
        glm::vec3 WorldUp;
        float MaxSpeed;
        float JumpPower;
        float AspectRatio;
        float NearPlane;
        float FarPlane;
        bool isAutomatic = false;
        glm::vec3 automaticMovementDirection = glm::vec3(0, 0, 0);
        float automaticMovementSpeed = 0;
        float DeltaTime = 0.0f;
        glm::vec3 ForcedVelocity = glm::vec3(0,0,0);
        glm::vec3 Force = glm::vec3(0, 0, 0);

        // Default constructor
        AbstractCamera(
            const glm::vec3& position = POSITION, 
            const glm::vec3& inputWorldUp = WORLDUP, 
            const glm::vec3& inputFront = FRONT, 
            float aspectRatio = ASPECTRATIO,
            const float& inputSpeed = 1000,
            const float& inputJumpPower= 100,
            float yawInput = YAW, 
            float pitchInput = PITCH, 
            float nearPlane = NEARPLANE, 
            float farPlane = FARPLANE
        ):
            Position(position),
            Up(inputWorldUp),
            Front(inputFront),
            WorldUp(inputWorldUp),
            MaxSpeed(inputSpeed),
            JumpPower(inputJumpPower),
            AspectRatio(aspectRatio),
            NearPlane(nearPlane),
            FarPlane(farPlane),
            FrameCounter(0),
            LastFrameTime(0.0f),
            DeltaTime(0.0f),
            CurrentFrameTime(0.0f),
            ForcedVelocity(glm::vec3(0,0,0))
        {
            // Calculate all position and orientation of camera
            UpdateCameraOrientationVectors();
            yaw = yawInput;
            pitch = pitchInput;
        }

        // returns the view matrix calculated from Euler Angles and the LookAt Matrix
        glm::mat4 GetViewMatrix() const 
        {
            return glm::lookAt(Position, Position + Front, Up);
        }

        // return projection matrix 
        glm::mat4 GetProjectionMatrix() const
        {
            return gcgCreatePerspectiveProjectionMatrix(glm::radians(fov), AspectRatio, NearPlane, FarPlane);
        }

        // returns front facing vector of the camera (camera direction)
        glm::vec3 GetFront() const
        {
            return Front;
        }

        void Update()
        {
            FrameCounter++;
            LastFrameTime = CurrentFrameTime;
            CurrentFrameTime = glfwGetTime();
            DeltaTime = CurrentFrameTime - LastFrameTime;
            if (!isAutomatic) UpdateCameraOrientationVectors();
        }

        glm::vec3 getPosition() const
        {
            return Position;
        }

        glm::mat4 getViewProjectionMatrix() const
        {
            //glm::mat4 trafo(glm::vec4(1, 0, 0, 0), glm::vec4(0, -1, 0, 0), glm::vec4(0, 0, 1, 0), glm::vec4(0, 0, 0, 1));
            return GetProjectionMatrix() * GetViewMatrix();
        }

    protected:
        // Variables for Camera Movement Calculation 
        int FrameCounter = 0.0f;
        float LastFrameTime = 0.0f;
        float CurrentFrameTime = 0.0f;

        // processes input received from any keyboard-like input system. Accepts input parameter in the form of camera defined ENUM (to abstract it from windowing systems)
        void Move(Camera_Movement direction)
        {
            glm::vec3 FrontProjectedToGround = glm::normalize(Front - glm::dot(Front, WorldUp) * WorldUp);
            if (direction == FORWARD)   ForcedVelocity += FrontProjectedToGround * MaxSpeed;
            if (direction == BACKWARD)  ForcedVelocity -= FrontProjectedToGround * MaxSpeed;
            if (direction == LEFT)      ForcedVelocity -= Right * MaxSpeed;
            if (direction == RIGHT)     ForcedVelocity += Right * MaxSpeed;
            if (direction == STOP)      ForcedVelocity = glm::vec3(0,0,0);
        }  

        void Jump()
        {  
            // Apply jump force until max speed for Jump is reached, after that jump force is 0
            Force = WorldUp * JumpPower;
        }

        // processes input received from a mouse input system. Expects the offset value in both the x and y direction.
        void Rotate(double xoffset, double yoffset, GLboolean constrainPitch = true)
        {
            xoffset *= angularSpeed;
            yoffset *= angularSpeed;

            yaw =0,// xoffset;
            pitch += yoffset;

            // Player can look up a bit more more than down
            if (constrainPitch)
            {
                if (pitch > 85.0f)
                    pitch = 85.0f;
                if (pitch < -70.0f)
                    pitch = -70.0f;
            }

            // update Front, Right and Up Vectors using the updated Euler angles
            UpdateCameraOrientationVectors();
        }

        // calculates the front, right and up vector from the Camera's (updated) Euler Angles
        void UpdateCameraOrientationVectors()
        {
            // calculate the new Front vector
            glm::vec3 direction;
            direction.x = cos(glm::radians(yaw)) * cos(glm::radians(pitch));
            direction.y = sin(glm::radians(pitch));
            direction.z = sin(glm::radians(yaw)) * cos(glm::radians(pitch));
            Front = glm::normalize(direction);
            // re-calculate the Right and Up vector
            Right = glm::normalize(glm::cross(Front, WorldUp));  // normalize the vectors, because their length gets closer to 0 the more you look up or down which results in slower movement.
            Up = glm::normalize(glm::cross(Right, Front));
        }
    };

    // A concrete camera class that implements camera system in glfw window . Inherits from AbstractCamera Class
    class CameraForGLFW : public AbstractCamera
    {
    public:
        CameraForGLFW(
            GLFWwindow* window, 
            const float& inputAspectRatio, 
            const glm::vec3& inputPosition, 
            const glm::vec3& inputWorldUp, 
            const glm::vec3& inputFront, 
            const float& inputMaxSpeed,
            const float& inputJumpPower,
            const bool captureMouse = true, 
            const bool fovCanBeChanged = false):
            AbstractCamera(inputPosition, inputWorldUp, inputFront, inputAspectRatio, inputMaxSpeed, inputJumpPower),
            Window(window)
        {
            // Capture mouse cursor, set it to center of window and hide it
            if (captureMouse) glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED);

            // Define our mouse_callback function as the callback for mouse movements
            glfwSetCursorPosCallback(window, mouse_callback);
        }

        ~CameraForGLFW()
        {
        }

        void updateCameraAtNewFrame()
        {
            // update orientation based on current yaw and pitch angle (which was updated already by mouse)
        Base: Update();
            if (!isAutomatic)
            {
                // Update position according to keyboard in directions front and  right
                processKeyboardInput();
            }
            else
            {
                glm::vec3 velocity = automaticMovementSpeed * glm::normalize(automaticMovementDirection);
                Position += DeltaTime * velocity;
            }
        }

        void checkIfUserSwitchedtoManulaMode(GLFWwindow* window, int keyCode = GLFW_KEY_ENTER)
        {
            // Esc Key: Close window
            // If Esc not pressed glfwGetKey returns GLFW_RELEASE
            if (glfwGetKey(window, keyCode) == GLFW_PRESS)
            {
                isAutomatic = false;
            }
        }

    protected:
        GLFWwindow* Window;

        void processKeyboardInput()
        {
            ForcedVelocity = glm::vec3(0, 0, 0);
            if (glfwGetKey(Window, GLFW_KEY_UP) == GLFW_PRESS || glfwGetKey(Window, GLFW_KEY_W) == GLFW_PRESS) Move(FORWARD);

            if (glfwGetKey(Window, GLFW_KEY_DOWN) == GLFW_PRESS || glfwGetKey(Window, GLFW_KEY_S) == GLFW_PRESS) Move(BACKWARD);

            if (glfwGetKey(Window, GLFW_KEY_LEFT) == GLFW_PRESS || glfwGetKey(Window, GLFW_KEY_A) == GLFW_PRESS) Move(LEFT);

            if (glfwGetKey(Window, GLFW_KEY_RIGHT) == GLFW_PRESS || glfwGetKey(Window, GLFW_KEY_D) == GLFW_PRESS) Move(RIGHT);

            if (glfwGetKey(Window, GLFW_KEY_SPACE) == GLFW_PRESS) Jump();
        }

        //function that is called on mouse movement: modifies cameraDirection, yaw, pitch
        static void mouse_callback(GLFWwindow* window, double mousePosX, double mousePosY)
        {
            if (firstMouseMove)
            {
                lastMouseX = mousePosX;
                lastMouseY = mousePosY;
                firstMouseMove = false;
            }
            mouseOffsetX = mousePosX - lastMouseX;
            mouseOffsetY = mousePosY - lastMouseY;
            lastMouseX = mousePosX;
            lastMouseY = mousePosY;
            yaw += (angularSpeed * mouseOffsetX);
            pitch -= (angularSpeed * mouseOffsetY);

            if (pitch > 89.0f)
                pitch = 89.0f;
            if (pitch < -89.0f)
                pitch = -89.0f;
        };
    };
}
#endif