// src/Camera.cpp

#include "Camera.h"
#include <glm/gtc/matrix_transform.hpp>
#include <iostream>

Camera::Camera(ProjectionType type,
               const glm::vec3& position,
               const glm::vec3& target,
               const glm::vec3& up)
    : projection_type_(type),
      position_(position),
      target_(target),
      up_(up)
{
    view_dirty_ = projection_dirty_ = true;
    UpdateFromEulerAngles();
}

void Camera::SetPosition(const glm::vec3& position) {
    position_ = position;
    view_dirty_ = true;
}

void Camera::SetTarget(const glm::vec3& target) {
    target_ = target;
    view_dirty_ = true;
}

void Camera::SetUp(const glm::vec3& up) {
    up_ = up;
    view_dirty_ = true;
}

void Camera::LookAt(const glm::vec3& position,
                    const glm::vec3& target,
                    const glm::vec3& up) {
    position_ = position;
    target_ = target;
    up_ = up;
    view_dirty_ = true;
}

void Camera::SetPerspective(float fov,
                            float aspect_ratio,
                            float near_plane,
                            float far_plane) {
    projection_type_ = ProjectionType::Perspective;
    fov_ = fov;
    aspect_ratio_ = aspect_ratio;
    near_plane_ = near_plane;
    far_plane_ = far_plane;
    projection_dirty_ = true;
}

void Camera::SetOrthographic(float left,
                             float right,
                             float bottom,
                             float top,
                             float near_plane,
                             float far_plane) {
    projection_type_ = ProjectionType::Orthographic;
    ortho_left_ = left;
    ortho_right_ = right;
    ortho_bottom_ = bottom;
    ortho_top_ = top;
    near_plane_ = near_plane;
    far_plane_ = far_plane;
    projection_dirty_ = true;
}

void Camera::SetAspectRatio(float aspect_ratio) {
    aspect_ratio_ = aspect_ratio;
    projection_dirty_ = true;
}

void Camera::Move(const glm::vec3& offset) {
    position_ += offset;
    target_   += offset;
    view_dirty_ = true;
}

void Camera::MoveForward(float distance) {
    glm::vec3 forward = glm::normalize(target_ - position_);
    Move(forward * distance);
}

void Camera::MoveBackward(float distance) { MoveForward(-distance); }
void Camera::MoveLeft(float distance)    { Move(glm::normalize(glm::cross(up_, target_ - position_)) * -distance); }
void Camera::MoveRight(float distance)   { Move(glm::normalize(glm::cross(up_, target_ - position_)) * distance); }
void Camera::MoveUp(float distance)      { Move(up_ * distance); }
void Camera::MoveDown(float distance)    { Move(-up_ * distance); }

void Camera::Rotate(float yaw, float pitch) {
    yaw_   += yaw;
    pitch_ += pitch;
    if (pitch_ > MAX_PITCH) pitch_ = MAX_PITCH;
    if (pitch_ < -MAX_PITCH) pitch_ = -MAX_PITCH;
    UpdateFromEulerAngles();
}

void Camera::SetYaw(float yaw)   { yaw_ = yaw; UpdateFromEulerAngles(); }
void Camera::SetPitch(float p)   { pitch_ = p; UpdateFromEulerAngles(); }

const glm::mat4& Camera::GetViewMatrix() const {
    if (view_dirty_) UpdateViewMatrix();
    return view_matrix_;
}

const glm::mat4& Camera::GetProjectionMatrix() const {
    if (projection_dirty_) UpdateProjectionMatrix();
    return projection_matrix_;
}

glm::mat4 Camera::GetViewProjectionMatrix() const {
    return GetProjectionMatrix() * GetViewMatrix();
}

glm::vec3 Camera::GetForward() const {
    return glm::normalize(target_ - position_);
}

glm::vec3 Camera::GetRight() const {
    return glm::normalize(glm::cross(GetForward(), up_));
}

glm::vec3 Camera::ScreenToWorld(const glm::vec2& screen_pos,
                                const glm::vec2& screen_size,
                                float depth) const {
    glm::vec4 ndc{
        (screen_pos.x / screen_size.x) * 2.0f - 1.0f,
        (screen_pos.y / screen_size.y) * 2.0f - 1.0f,
        depth * 2.0f - 1.0f,
        1.0f
    };
    glm::mat4 invVP = glm::inverse(GetViewProjectionMatrix());
    glm::vec4 world = invVP * ndc;
    return glm::vec3(world) / world.w;
}

glm::vec2 Camera::WorldToScreen(const glm::vec3& world_pos,
                                const glm::vec2& screen_size) const {
    glm::vec4 clip = GetViewProjectionMatrix() * glm::vec4(world_pos, 1.0f);
    glm::vec3 ndc = glm::vec3(clip) / clip.w;
    return {
        ((ndc.x + 1.0f) * 0.5f) * screen_size.x,
        ((ndc.y + 1.0f) * 0.5f) * screen_size.y
    };
}

void Camera::PrintInfo() const {
    std::cout << "Camera Position: (" 
              << position_.x << ", " << position_.y << ", " << position_.z << ")\n"
              << "Yaw: " << yaw_ << ", Pitch: " << pitch_ << "\n";
}

void Camera::UpdateViewMatrix() const {
    view_matrix_ = glm::lookAt(position_, target_, up_);
    view_dirty_ = false;
}

void Camera::UpdateProjectionMatrix() const {
    if (projection_type_ == ProjectionType::Perspective) {
        projection_matrix_ = glm::perspective(
            glm::radians(fov_),
            aspect_ratio_,
            near_plane_,
            far_plane_
        );
    } else {
        projection_matrix_ = glm::ortho(
            ortho_left_, ortho_right_, ortho_bottom_, ortho_top_,
            near_plane_, far_plane_
        );
    }
    projection_dirty_ = false;
}

void Camera::UpdateFromEulerAngles() {
    glm::vec3 dir;
    dir.x = cos(glm::radians(yaw_)) * cos(glm::radians(pitch_));
    dir.y = sin(glm::radians(pitch_));
    dir.z = sin(glm::radians(yaw_)) * cos(glm::radians(pitch_));
    target_ = position_ + glm::normalize(dir);
    view_dirty_ = true;
}
