#pragma once

#include <math.h>

#include <box3d/math_functions.h>

#include "crStruct.h"

namespace crMath
{
    constexpr float PI = B3_PI;

    // libm sqrtf/roundf — same calls box3d itself makes; IEEE-754 specifies both exactly, so they are bit-identical across platforms
    inline float Sqrt( float v )
    {
        return sqrtf( v );
    }
    inline float Round( float v )
    {
        return roundf( v );
    }

    // libm powf — render-only: IEEE-754 does NOT pin pow results, keep it out of the sim.
    // for a growth curve inside a fixed step, multiply repeatedly instead ( for( i < n ) v *= step; ) —
    // every multiply IS pinned, and the exponents games need are small
    inline float Pow( float base, float exponent )
    {
        return powf( base, exponent );
    }
    // render-only (Pow) — matches the shader-side srgbToLinear; alpha passes through
    inline color4 SrgbToLinear( color4 c )
    {
        return color4( Pow( c.r, 2.2f ), Pow( c.g, 2.2f ), Pow( c.b, 2.2f ), c.a );
    }

    inline float Sin( float radians )
    {
        return b3ComputeCosSin( radians ).sine;
    }
    inline float Cos( float radians )
    {
        return b3ComputeCosSin( radians ).cosine;
    }
    inline float2 CosSin( float radians )
    {
        const b3CosSin cs = b3ComputeCosSin( radians );
        return float2( cs.cosine, cs.sine );
    }
    inline float Tan( float radians )
    {
        const b3CosSin cs = b3ComputeCosSin( radians );
        return cs.sine / cs.cosine;
    }
    inline float Asin( float s )
    {
        if( s < -1.0f )
            s = -1.0f;
        else if( s > 1.0f )
            s = 1.0f;
        return b3Atan2( s, Sqrt( 1.0f - ( s * s ) ) );
    }
    inline float Acos( float c )
    {
        if( c < -1.0f )
            c = -1.0f;
        else if( c > 1.0f )
            c = 1.0f;
        return b3Atan2( Sqrt( 1.0f - ( c * c ) ), c );
    }
    inline float Atan( float t )
    {
        return b3Atan2( t, 1.0f );
    }
    inline float Atan2( float y, float x )
    {
        return b3Atan2( y, x );
    }

    // aspect-fit factor for the vertical extent (camera fov, ui canvas): the axis that did not
    // grow stays pinned to the reference, the other one reveals more. referenceAspect 0 = off
    inline float AspectGrowth( float aspect, float referenceAspect )
    {
        if( ( referenceAspect <= 0.0f ) || ( aspect <= 0.0f ) || ( aspect >= referenceAspect ) )
            return 1.0f;

        return ( referenceAspect / aspect );
    }

    inline float Abs( float f )
    {
        return b3AbsFloat( f );
    }
    inline float2 Abs2( float2 xy )
    {
        return float2( b3AbsFloat( xy.x ), b3AbsFloat( xy.y ) );
    }

    inline float Clamp01( float f )
    {
        if( f < 0.0f )
            return 0.0f;

        if( f > 1.0f )
            return 1.0f;

        return f;
    }
    inline float Clamp( float f, float min, float max )
    {
        if( f < min )
            return min;

        if( f > max )
            return max;

        return f;
    }
    inline float Lerp( float a, float b, float alpha )
    {
        return b3LerpFloat( a, b, alpha );
    }

    inline float Dot3( float3 a, float3 b )
    {
        return ( a.x * b.x ) + ( a.y * b.y ) + ( a.z * b.z );
    }
    inline float3 Cross3( float3 a, float3 b )
    {
        return float3( ( a.y * b.z ) - ( a.z * b.y ),
                       ( a.z * b.x ) - ( a.x * b.z ),
                       ( a.x * b.y ) - ( a.y * b.x ) );
    }
    inline float3 Normalize3( float3 v )   // zero-length input falls back to (0,0,1)
    {
        const float len = Sqrt( Dot3( v, v ) );
        if( len < 0.000001f )
            return float3( 0.0f, 0.0f, 1.0f );

        return float3( v.x / len, v.y / len, v.z / len );
    }
    inline float3 Lerp3( float3 a, float3 b, float alpha )
    {
        return b3Lerp( a.b3(), b.b3(), alpha );
    }
    inline float2 ClampLength2( float2 v, float maxLen )
    {
        const float sq = ( v.x * v.x ) + ( v.y * v.y );
        if( sq <= ( maxLen * maxLen ) )
            return v;

        const float scale = maxLen / Sqrt( sq );
        return float2( v.x * scale, v.y * scale );
    }
    inline quat4 NLerp( b3Quat a, b3Quat b, float alpha )   // shortest-arc, renormalized — sim quats in, engine quat out
    {
        return b3NLerp( a, b, alpha );
    }
    inline b3Quat QuatFromAxisAngle( float3 axis, float radians )   // the axis need not be unit length
    {
        const float3 n  = Normalize3( axis );
        const float2 cs = CosSin( radians * 0.5f );
        return { { n.x * cs.y, n.y * cs.y, n.z * cs.y }, cs.x };
    }
    inline color4 LerpColor( color4 a, color4 b, float alpha )
    {
        return color4( b3LerpFloat( a.r, b.r, alpha ),
                       b3LerpFloat( a.g, b.g, alpha ),
                       b3LerpFloat( a.b, b.b, alpha ),
                       b3LerpFloat( a.a, b.a, alpha ) );
    }

    float4x4 Mult4x4( const float4x4& a, const float4x4& b );   // column-major: r = a * b
    float4x4 OrthoRH( float halfWidth, float halfHeight, float nearPlane, float farPlane );   // symmetric, GL clip space
    float4x4 LookAtRH( float3 eye, float3 target, float3 up );

    // frustum culling, render-only — the extraction works for an ortho projection as well as a
    // perspective one (half-space convention: see crPlane)
    crFrustum FrustumFromMatrix( const float4x4& viewProjection );
    inline bool SphereInFrustum( const crFrustum& frustum, float3 center, float radius )
    {
        for( int32_t i = 0; i < 6; ++i )
        {
            const float dist = ( Dot3( frustum.planes[ i ].n, center ) + frustum.planes[ i ].d );
            if( dist < -radius )
                return false;   // fully outside this plane -> outside the volume
        }
        return true;
    }

    // advance a rotation by angular velocity over dt — the b2IntegrateRotation analogue (box3d does not expose one);
    // built purely from b3 ops (+,*,sqrt) so it stays cross-platform deterministic
    b3Quat IntegrateRotation( b3Quat q, float3 angularVelocity, float dt );

    // FNV-1a — integer-only, platform-independent; chain calls by passing the previous result as seed
    constexpr uint64_t HASH_FNV1A_SEED = 14695981039346656037ULL;

    uint64_t HashFnv1a( const void* data, size_t size, uint64_t seed );
}
