#include "crCamera.h"

#include "crMath.h"
#include "crScreenRef.h"

void crCamera::Init()
{
    RebuildPoseFromRig();
}
void crCamera::Cleanup()
{

}

bool crCamera::SetViewport( int2 pixelSize )
{
    if( ( _viewport.x == pixelSize.x ) && ( _viewport.y == pixelSize.y ) )
    {
        return false;
    }

    _viewport = pixelSize;

    return true;
}
void crCamera::SetDistance( float distance )
{
    _distance = crMath::Clamp( distance, DISTANCE_MIN, DISTANCE_MAX );
    RebuildPoseFromRig();
}
void crCamera::SetTarget( float3 target )
{
    _target = target;
    RebuildPoseFromRig();
}
void crCamera::SetOrbit( float yaw, float pitch )
{
    _yaw   = yaw;
    _pitch = crMath::Clamp( pitch, -PITCH_LIMIT, PITCH_LIMIT );
    RebuildPoseFromRig();
}
void crCamera::SetRoll( float radians )
{
    _roll = radians;
    RebuildPoseFromRig();
}
void crCamera::SetWorldTransform( b3WorldTransform pose )
{
    _pose = pose;
}
void crCamera::SetFovY( float radians )
{
    _fovY = crMath::Clamp( radians, FOV_Y_MIN, FOV_Y_MAX );
}
void crCamera::SetClip( float nearPlane, float farPlane )
{
    _nearPlane = crMath::Clamp( nearPlane, 0.01f, 5000.0f );
    _farPlane  = crMath::Clamp( farPlane, ( _nearPlane + 0.1f ), 10000.0f );
}

void crCamera::Orbit( float dyaw, float dpitch )
{
    _yaw   += dyaw;
    _pitch  = crMath::Clamp( _pitch + dpitch, -PITCH_LIMIT, PITCH_LIMIT );
    RebuildPoseFromRig();
}
void crCamera::Look( float dyaw, float dpitch )
{
    const float3 eye = RigEye();

    _yaw   += dyaw;
    _pitch  = crMath::Clamp( _pitch + dpitch, -PITCH_LIMIT, PITCH_LIMIT );

    const float3 dir = OrbitDir();
    _target = float3( eye.x - ( dir.x * _distance ),
                      eye.y - ( dir.y * _distance ),
                      eye.z - ( dir.z * _distance ) );
    RebuildPoseFromRig();
}
void crCamera::Pan( float dxPixels, float dyPixels )
{
    // world height covered by the viewport at the target distance -> world units per pixel
    const float worldPerPixel = ( 2.0f * _distance * crMath::Tan( EffectiveFovY() * 0.5f ) ) / static_cast<float>( _viewport.y );

    const crBasis basis = OrbitBasis();

    // content follows the cursor: drag right -> target moves left; drag down (dy+) -> target moves up
    _target.x += ( ( -basis.right.x * dxPixels ) + ( basis.up.x * dyPixels ) ) * worldPerPixel;
    _target.y += ( ( -basis.right.y * dxPixels ) + ( basis.up.y * dyPixels ) ) * worldPerPixel;
    _target.z += ( ( -basis.right.z * dxPixels ) + ( basis.up.z * dyPixels ) ) * worldPerPixel;
    RebuildPoseFromRig();
}
void crCamera::Fly( float3 localDir, float distance )
{
    const crBasis basis = OrbitBasis();

    _target.x += ( ( basis.right.x * localDir.x ) + ( basis.up.x * localDir.y ) + ( basis.forward.x * localDir.z ) ) * distance;
    _target.y += ( ( basis.right.y * localDir.x ) + ( basis.up.y * localDir.y ) + ( basis.forward.y * localDir.z ) ) * distance;
    _target.z += ( ( basis.right.z * localDir.x ) + ( basis.up.z * localDir.y ) + ( basis.forward.z * localDir.z ) ) * distance;
    RebuildPoseFromRig();
}

void crCamera::Dolly( float factor )
{
    _distance = crMath::Clamp( _distance * factor, DISTANCE_MIN, DISTANCE_MAX );
    RebuildPoseFromRig();
}

float3 crCamera::EyePosition() const
{
    return float3( _pose.p );
}

float3 crCamera::Forward() const
{
    return PoseBasis().forward;
}

float crCamera::EffectiveFovY() const
{
    const float aspect = static_cast<float>( _viewport.x ) / static_cast<float>( _viewport.y );
    const float grow   = crMath::AspectGrowth( aspect, crScreenRef::ASPECT );
    if( grow <= 1.0f )
        return _fovY;

    return crMath::Clamp( 2.0f * crMath::Atan( crMath::Tan( _fovY * 0.5f ) * grow ), FOV_Y_MIN, FOV_Y_MAX );
}

b3WorldTransform crCamera::WorldTransform() const
{
    return _pose;
}

float4x4 crCamera::PixelProjection() const
{
    // Y-up bottom-left so the sprite quad (Y-up) is not vertically flipped; text layout converts top-left input
    const float w = static_cast<float>( _viewport.x );
    const float h = static_cast<float>( _viewport.y );

    float4x4 r = {};
    r.m[ 0 ]  = 2.0f / w;
    r.m[ 5 ]  = 2.0f / h;
    r.m[ 10 ] = 1.0f;
    r.m[ 12 ] = -1.0f;
    r.m[ 13 ] = -1.0f;
    r.m[ 15 ] = 1.0f;
    return r;
}
float4x4 crCamera::ViewProjection() const
{
    // perspective (right-handed, GL clip space)
    const float aspect = static_cast<float>( _viewport.x ) / static_cast<float>( _viewport.y );
    const float f      = 1.0f / crMath::Tan( EffectiveFovY() * 0.5f );

    float4x4 p = {};
    p.m[ 0 ]  = f / aspect;
    p.m[ 5 ]  = f;
    p.m[ 10 ] = ( _farPlane + _nearPlane ) / ( _nearPlane - _farPlane );
    p.m[ 11 ] = -1.0f;
    p.m[ 14 ] = ( 2.0f * _farPlane * _nearPlane ) / ( _nearPlane - _farPlane );

    // view = pose basis transposed into rows, eye folded into the translation
    const float3  eye   = float3( _pose.p );
    const crBasis basis = PoseBasis();

    float4x4 v = {};
    v.m[ 0 ] = basis.right.x;
    v.m[ 4 ] = basis.right.y;
    v.m[ 8 ] = basis.right.z;
    v.m[ 1 ] = basis.up.x;
    v.m[ 5 ] = basis.up.y;
    v.m[ 9 ] = basis.up.z;
    v.m[ 2 ]  = -basis.forward.x;
    v.m[ 6 ]  = -basis.forward.y;
    v.m[ 10 ] = -basis.forward.z;
    v.m[ 12 ] = -crMath::Dot3( basis.right, eye );
    v.m[ 13 ] = -crMath::Dot3( basis.up, eye );
    v.m[ 14 ] = crMath::Dot3( basis.forward, eye );
    v.m[ 15 ] = 1.0f;

    return crMath::Mult4x4( p, v );
}

crScreenPoint crCamera::WorldToScreen( float3 world ) const
{
    const float4x4 vp = ViewProjection();

    const float cx = ( vp.m[ 0 ] * world.x ) + ( vp.m[ 4 ] * world.y ) + ( vp.m[ 8 ] * world.z ) + vp.m[ 12 ];
    const float cy = ( vp.m[ 1 ] * world.x ) + ( vp.m[ 5 ] * world.y ) + ( vp.m[ 9 ] * world.z ) + vp.m[ 13 ];
    const float cw = ( vp.m[ 3 ] * world.x ) + ( vp.m[ 7 ] * world.y ) + ( vp.m[ 11 ] * world.z ) + vp.m[ 15 ];

    crScreenPoint r;
    if( cw <= 0.0f )
        return r;   // behind the camera

    const float ndcx = cx / cw;
    const float ndcy = cy / cw;

    r.pos.x   = ( ndcx + 1.0f ) * 0.5f * static_cast<float>( _viewport.x );
    r.pos.y   = ( 1.0f - ndcy ) * 0.5f * static_cast<float>( _viewport.y );
    r.visible = true;
    return r;
}

crRay crCamera::ScreenToRay( float2 pixel ) const
{
    const float ndcX = ( ( pixel.x / static_cast<float>( _viewport.x ) ) * 2.0f ) - 1.0f;
    const float ndcY = 1.0f - ( ( pixel.y / static_cast<float>( _viewport.y ) ) * 2.0f );

    const float aspect = static_cast<float>( _viewport.x ) / static_cast<float>( _viewport.y );
    const float tanH   = crMath::Tan( EffectiveFovY() * 0.5f );

    const crBasis basis = PoseBasis();

    const float rx = ndcX * tanH * aspect;
    const float uy = ndcY * tanH;

    crRay ray;
    ray.origin = float3( _pose.p );
    ray.dir    = crMath::Normalize3( float3( basis.forward.x + ( basis.right.x * rx ) + ( basis.up.x * uy ),
                                             basis.forward.y + ( basis.right.y * rx ) + ( basis.up.y * uy ),
                                             basis.forward.z + ( basis.right.z * rx ) + ( basis.up.z * uy ) ) );
    return ray;
}

crPlaneHit crCamera::ScreenToPlaneZ0( float2 pixel ) const
{
    const crRay ray = ScreenToRay( pixel );

    crPlaneHit hit;
    if( crMath::Abs( ray.dir.z ) < 0.0001f )
        return hit;   // parallel to the plane

    const float t = ( 0.0f - ray.origin.z ) / ray.dir.z;
    if( t <= 0.0f )
        return hit;   // plane is behind the eye

    hit.pos   = float3( ray.origin.x + ( ray.dir.x * t ), ray.origin.y + ( ray.dir.y * t ), 0.0f );
    hit.valid = true;
    return hit;
}

float3 crCamera::OrbitDir() const
{
    const float2 cp = crMath::CosSin( _pitch );
    const float2 cy = crMath::CosSin( _yaw );

    // yaw 0, pitch 0 -> dir (0,0,1): eye sits on +Z looking toward -Z; +pitch raises the eye (looks down)
    return float3( cp.x * cy.y, cp.y, cp.x * cy.x );
}
float3 crCamera::RigEye() const
{
    const float3 dir = OrbitDir();

    return float3( _target.x + ( dir.x * _distance ),
                   _target.y + ( dir.y * _distance ),
                   _target.z + ( dir.z * _distance ) );
}
crBasis crCamera::OrbitBasis() const
{
    const float3 dir = OrbitDir();

    crBasis basis;
    basis.forward = float3( -dir.x, -dir.y, -dir.z );
    basis.right   = crMath::Normalize3( crMath::Cross3( basis.forward, float3( 0.0f, 1.0f, 0.0f ) ) );
    basis.up      = crMath::Cross3( basis.right, basis.forward );

    if( _roll != 0.0f )
    {// roll turns the frame about the view axis (local +Z = -forward), right-hand rule
        const float2 cs    = crMath::CosSin( _roll );
        const float3 right = basis.right;
        const float3 up    = basis.up;

        basis.right = float3( ( right.x * cs.x ) + ( up.x * cs.y ),
                              ( right.y * cs.x ) + ( up.y * cs.y ),
                              ( right.z * cs.x ) + ( up.z * cs.y ) );
        basis.up    = float3( ( up.x * cs.x ) - ( right.x * cs.y ),
                              ( up.y * cs.x ) - ( right.y * cs.y ),
                              ( up.z * cs.x ) - ( right.z * cs.y ) );
    }

    return basis;
}
crBasis crCamera::PoseBasis() const
{
    const b3Vec3 z = b3RotateVector( _pose.q, { 0.0f, 0.0f, 1.0f } );

    crBasis basis;
    basis.right   = b3RotateVector( _pose.q, { 1.0f, 0.0f, 0.0f } );
    basis.up      = b3RotateVector( _pose.q, { 0.0f, 1.0f, 0.0f } );
    basis.forward = float3( -z.x, -z.y, -z.z );
    return basis;
}
void crCamera::RebuildPoseFromRig()
{
    crBasis      basis = OrbitBasis();
    const float3 eye   = RigEye();

    b3Matrix3 m;
    m.cx = basis.right.b3();
    m.cy = basis.up.b3();
    m.cz = { -basis.forward.x, -basis.forward.y, -basis.forward.z };

    _pose.p = { eye.x, eye.y, eye.z };
    _pose.q = b3MakeQuatFromMatrix( &m );
}
