// camera3d.cpp - Implementació de la càmera 3D amb projecció en CPU
// © 2026 JailDesigner

#include "core/graphics/camera3d.hpp"

#include <cmath>

namespace Graphics {

    Camera3D::Camera3D(const Vec3& position, const Vec3& target, const Vec3& up_world, float fov_y_rad, float viewport_w, float viewport_h, float near_plane, float far_plane)
        : position_(position),
          target_(target),
          up_world_(up_world),
          fov_y_rad_(fov_y_rad),
          viewport_w_(viewport_w),
          viewport_h_(viewport_h),
          near_(near_plane),
          far_(far_plane) {
        recomputeBasis();
        recomputeFocal();
    }

    void Camera3D::setPosition(const Vec3& p) {
        position_ = p;
        recomputeBasis();
    }

    void Camera3D::setTarget(const Vec3& t) {
        target_ = t;
        recomputeBasis();
    }

    void Camera3D::setUpWorld(const Vec3& u) {
        up_world_ = u;
        recomputeBasis();
    }

    void Camera3D::setViewport(float w, float h) {
        viewport_w_ = w;
        viewport_h_ = h;
        recomputeFocal();
    }

    void Camera3D::setFovY(float fov_y_rad) {
        fov_y_rad_ = fov_y_rad;
        recomputeFocal();
    }

    void Camera3D::recomputeBasis() {
        // Forward = del position cap al target.
        forward_ = (target_ - position_).normalized();
        // Right = up_world × forward (convenció right-handed amb Y up,
        // mirant cap a +Z → right cau a +X). L'invers (forward × up_world)
        // donava la base mirall i invertia l'eix X de la projecció.
        right_ = up_world_.cross(forward_).normalized();
        // Up ortogonal real = forward × right (manté la mà dreta).
        up_ = forward_.cross(right_).normalized();
    }

    void Camera3D::recomputeFocal() {
        // Focal length en píxels: (viewport_height / 2) / tan(fov_y / 2).
        // Assumeix píxels quadrats (focal_x == focal_y).
        const float HALF_FOV = fov_y_rad_ * 0.5F;
        const float TAN_HALF = std::tan(HALF_FOV);
        focal_ = (TAN_HALF > 0.0F) ? ((viewport_h_ * 0.5F) / TAN_HALF) : 0.0F;
        centre_x_ = viewport_w_ * 0.5F;
        centre_y_ = viewport_h_ * 0.5F;
    }

    auto Camera3D::project(const Vec3& world) const -> std::optional<ProjectedPoint> {
        const Vec3 REL = world - position_;
        const float CX = REL.dot(right_);
        const float CY = REL.dot(up_);
        const float CZ = REL.dot(forward_);

        if (CZ <= near_) {
            return std::nullopt;
        }

        const float SCALE = focal_ / CZ;
        return ProjectedPoint{
            .screen = Vec2{
                .x = centre_x_ + (CX * SCALE),
                // Flip Y: en pantalla Y creix cap avall.
                .y = centre_y_ - (CY * SCALE),
            },
            .scale = SCALE,
            .depth = CZ,
        };
    }

}  // namespace Graphics
