// file: source/math.c

#ifdef ZPL_EDITOR
#include <zpl.h>
#endif


#if !defined(ZPL_NO_MATH_H)
    #include <math.h>
#endif

ZPL_BEGIN_C_DECLS

////////////////////////////////////////////////////////////////
//
// Math
//

/* NOTE: To remove the need for memcpy */
static void zpl__memcpy_4byte(void *dest, void const *src, zpl_isize size) {
    zpl_isize i;
    unsigned int *d, *s;
    d = (unsigned int *)dest;
    s = (unsigned int *)src;
    for (i = 0; i < size / 4; i++) { *d++ = *s++; }
}

zpl_f32 zpl_to_radians(zpl_f32 degrees) { return degrees * ZPL_TAU / 360.0f; }
zpl_f32 zpl_to_degrees(zpl_f32 radians) { return radians * 360.0f / ZPL_TAU; }

zpl_f32 zpl_angle_diff(zpl_f32 radians_a, zpl_f32 radians_b) {
    zpl_f32 delta = zpl_mod(radians_b - radians_a, ZPL_TAU);
    delta = zpl_mod(delta + 1.5f * ZPL_TAU, ZPL_TAU);
    delta -= 0.5f * ZPL_TAU;
    return delta;
}

zpl_f32 zpl_copy_sign(zpl_f32 x, zpl_f32 y) {
    int ix, iy;
    ix = *(int *)&x;
    iy = *(int *)&y;

    ix &= 0x7fffffff;
    ix |= iy & 0x80000000;
    return *(zpl_f32 *)&ix;
}

zpl_f32 zpl_remainder(zpl_f32 x, zpl_f32 y) { return x - (zpl_round(x / y) * y); }

zpl_f32 zpl_mod(zpl_f32 x, zpl_f32 y) {
    zpl_f32 result;
    y = zpl_abs(y);
    result = zpl_remainder(zpl_abs(x), y);
    if (zpl_sign(result)) result += y;
    return zpl_copy_sign(result, x);
}

zpl_f64 zpl_copy_sign64(zpl_f64 x, zpl_f64 y) {
    zpl_i64 ix, iy;
    ix = *(zpl_i64 *)&x;
    iy = *(zpl_i64 *)&y;

    ix &= 0x7fffffffffffffff;
    ix |= iy & 0x8000000000000000;
    return *cast(zpl_f64 *) & ix;
}

zpl_f64 zpl_floor64(zpl_f64 x)                { return cast(zpl_f64)((x >= 0.0) ? cast(zpl_i64) x : cast(zpl_i64)(x - 0.9999999999999999)); }
zpl_f64 zpl_ceil64(zpl_f64 x)                 { return cast(zpl_f64)((x < 0) ? cast(zpl_i64) x : (cast(zpl_i64) x) + 1); }
zpl_f64 zpl_round64(zpl_f64 x)                { return cast(zpl_f64)((x >= 0.0) ? zpl_floor64(x + 0.5) : zpl_ceil64(x - 0.5)); }
zpl_f64 zpl_remainder64(zpl_f64 x, zpl_f64 y) { return x - (zpl_round64(x / y) * y); }
zpl_f64 zpl_abs64(zpl_f64 x)                  { return x < 0 ? -x : x; }
zpl_f64 zpl_sign64(zpl_f64 x)                 { return x < 0 ? -1.0 : +1.0; }

zpl_f64 zpl_mod64(zpl_f64 x, zpl_f64 y) {
    zpl_f64 result;
    y = zpl_abs64(y);
    result = zpl_remainder64(zpl_abs64(x), y);
    if (zpl_sign64(result)) result += y;
    return zpl_copy_sign64(result, x);
}

zpl_f32 zpl_quake_rsqrt(zpl_f32 a) {
    union {
        int i;
        zpl_f32 f;
    } t;
    zpl_f32 x2;
    zpl_f32 const three_halfs = 1.5f;

    x2 = a * 0.5f;
    t.f = a;
    t.i = 0x5f375a86 - (t.i >> 1);                /* What the fuck? */
    t.f = t.f * (three_halfs - (x2 * t.f * t.f)); /* 1st iteration */
    t.f = t.f * (three_halfs - (x2 * t.f * t.f)); /* 2nd iteration, this can be removed */

    return t.f;
}

#if defined(ZPL_NO_MATH_H)
    #if defined(_MSC_VER)

        zpl_f32 zpl_rsqrt(zpl_f32 a) { return _mm_cvtss_f32(_mm_rsqrt_ss(_mm_set_ss(a))); }
        zpl_f32 zpl_sqrt(zpl_f32 a)  { return _mm_cvtss_f32(_mm_sqrt_ss(_mm_set_ss(a))); };

        zpl_f32 zpl_sin(zpl_f32 a) {
            static zpl_f32 const a0 = +1.91059300966915117e-31f;
            static zpl_f32 const a1 = +1.00086760103908896f;
            static zpl_f32 const a2 = -1.21276126894734565e-2f;
            static zpl_f32 const a3 = -1.38078780785773762e-1f;
            static zpl_f32 const a4 = -2.67353392911981221e-2f;
            static zpl_f32 const a5 = +2.08026600266304389e-2f;
            static zpl_f32 const a6 = -3.03996055049204407e-3f;
            static zpl_f32 const a7 = +1.38235642404333740e-4f;
            return a0 + a * (a1 + a * (a2 + a * (a3 + a * (a4 + a * (a5 + a * (a6 + a * a7))))));
        }

        zpl_f32 zpl_cos(zpl_f32 a) {
            static zpl_f32 const a0 = +1.00238601909309722f;
            static zpl_f32 const a1 = -3.81919947353040024e-2f;
            static zpl_f32 const a2 = -3.94382342128062756e-1f;
            static zpl_f32 const a3 = -1.18134036025221444e-1f;
            static zpl_f32 const a4 = +1.07123798512170878e-1f;
            static zpl_f32 const a5 = -1.86637164165180873e-2f;
            static zpl_f32 const a6 = +9.90140908664079833e-4f;
            static zpl_f32 const a7 = -5.23022132118824778e-14f;
            return a0 + a * (a1 + a * (a2 + a * (a3 + a * (a4 + a * (a5 + a * (a6 + a * a7))))));
        }

        zpl_f32 zpl_tan(zpl_f32 radians) {
            zpl_f32 rr = radians * radians;
            zpl_f32 a = 9.5168091e-03f;
            a *= rr;
            a += 2.900525e-03f;
            a *= rr;
            a += 2.45650893e-02f;
            a *= rr;
            a += 5.33740603e-02f;
            a *= rr;
            a += 1.333923995e-01f;
            a *= rr;
            a += 3.333314036e-01f;
            a *= rr;
            a += 1.0f;
            a *= radians;
            return a;
        }

        zpl_f32 zpl_arcsin(zpl_f32 a) { return zpl_arctan2(a, zpl_sqrt((1.0f + a) * (1.0f - a))); }
        zpl_f32 zpl_arccos(zpl_f32 a) { return zpl_arctan2(zpl_sqrt((1.0f + a) * (1.0f - a)), a); }

        zpl_f32 zpl_arctan(zpl_f32 a) {
            zpl_f32 u = a * a;
            zpl_f32 u2 = u * u;
            zpl_f32 u3 = u2 * u;
            zpl_f32 u4 = u3 * u;
            zpl_f32 f = 1.0f + 0.33288950512027f * u - 0.08467922817644f * u2 + 0.03252232640125f * u3 - 0.00749305860992f * u4;
            return a / f;
        }

        zpl_f32 zpl_arctan2(zpl_f32 y, zpl_f32 x) {
            if (zpl_abs(x) > zpl_abs(y)) {
                zpl_f32 a = zpl_arctan(y / x);
                if (x > 0.0f)
                    return a;
                else
                    return y > 0.0f ? a + ZPL_TAU_OVER_2 : a - ZPL_TAU_OVER_2;
            } else {
                zpl_f32 a = zpl_arctan(x / y);
                if (x > 0.0f)
                    return y > 0.0f ? ZPL_TAU_OVER_4 - a : -ZPL_TAU_OVER_4 - a;
                else
                    return y > 0.0f ? ZPL_TAU_OVER_4 + a : -ZPL_TAU_OVER_4 + a;
            }
        }

        zpl_f32 zpl_exp(zpl_f32 a) {
            union {
                zpl_f32 f;
                int i;
            } u, v;
            u.i = (int)(6051102 * a + 1056478197);
            v.i = (int)(1056478197 - 6051102 * a);
            return u.f / v.f;
        }

        zpl_f32 zpl_log(zpl_f32 a) {
            union {
                zpl_f32 f;
                int i;
            } u = { a };
            return (u.i - 1064866805) * 8.262958405176314e-8f; /* 1 / 12102203.0; */
        }

        zpl_f32 zpl_pow(zpl_f32 a, zpl_f32 b) {
            int flipped = 0, e;
            zpl_f32 f, r = 1.0f;
            if (b < 0) {
                flipped = 1;
                b = -b;
            }

            e = (int)b;
            f = zpl_exp(b - e);

            while (e) {
                if (e & 1) r *= a;
                a *= a;
                e >>= 1;
            }

            r *= f;
            return flipped ? 1.0f / r : r;
        }

    #else

        zpl_f32 zpl_rsqrt(zpl_f32 a)              { return 1.0f / __builtin_sqrt(a); }
        zpl_f32 zpl_sqrt(zpl_f32 a)               { return __builtin_sqrt(a); }
        zpl_f32 zpl_sin(zpl_f32 radians)          { return __builtin_sinf(radians); }
        zpl_f32 zpl_cos(zpl_f32 radians)          { return __builtin_cosf(radians); }
        zpl_f32 zpl_tan(zpl_f32 radians)          { return __builtin_tanf(radians); }
        zpl_f32 zpl_arcsin(zpl_f32 a)             { return __builtin_asinf(a); }
        zpl_f32 zpl_arccos(zpl_f32 a)             { return __builtin_acosf(a); }
        zpl_f32 zpl_arctan(zpl_f32 a)             { return __builtin_atanf(a); }
        zpl_f32 zpl_arctan2(zpl_f32 y, zpl_f32 x) { return __builtin_atan2f(y, x); }

        zpl_f32 zpl_exp(zpl_f32 x) { return __builtin_expf(x); }
        zpl_f32 zpl_log(zpl_f32 x) { return __builtin_logf(x); }

        // TODO: Should this be zpl_exp(y * zpl_log(x)) ???
        zpl_f32 zpl_pow(zpl_f32 x, zpl_f32 y) { return __builtin_powf(x, y); }

    #endif
#else
    zpl_f32 zpl_rsqrt(zpl_f32 a)                { return 1.0f / sqrtf(a); }
    zpl_f32 zpl_sqrt(zpl_f32 a)                 { return sqrtf(a); };
    zpl_f32 zpl_sin(zpl_f32 radians)            { return sinf(radians); };
    zpl_f32 zpl_cos(zpl_f32 radians)            { return cosf(radians); };
    zpl_f32 zpl_tan(zpl_f32 radians)            { return tanf(radians); };
    zpl_f32 zpl_arcsin(zpl_f32 a)               { return asinf(a); };
    zpl_f32 zpl_arccos(zpl_f32 a)               { return acosf(a); };
    zpl_f32 zpl_arctan(zpl_f32 a)               { return atanf(a); };
    zpl_f32 zpl_arctan2(zpl_f32 y, zpl_f32 x)   { return atan2f(y, x); };

    zpl_f32 zpl_exp(zpl_f32 x)            { return expf(x); }
    zpl_f32 zpl_log(zpl_f32 x)            { return logf(x); }
    zpl_f32 zpl_pow(zpl_f32 x, zpl_f32 y) { return powf(x, y); }
#endif

zpl_f32 zpl_exp2(zpl_f32 x) { return zpl_exp(ZPL_LOG_TWO * x); }
zpl_f32 zpl_log2(zpl_f32 x) { return zpl_log(x) / ZPL_LOG_TWO; }

zpl_f32 zpl_fast_exp(zpl_f32 x) {
    /* NOTE: Only works in the range -1 <= x <= +1 */
    zpl_f32 e = 1.0f + x * (1.0f + x * 0.5f * (1.0f + x * 0.3333333333f * (1.0f + x * 0.25f * (1.0f + x * 0.2f))));
    return e;
}

zpl_f32 zpl_fast_exp2(zpl_f32 x) { return zpl_fast_exp(ZPL_LOG_TWO * x); }

zpl_f32 zpl_round(zpl_f32 x) { return (float)((x >= 0.0f) ? zpl_floor(x + 0.5f) : zpl_ceil(x - 0.5f)); }
zpl_f32 zpl_floor(zpl_f32 x) { return (float)((x >= 0.0f) ? (int)x              : (int)(x - 0.9999999999999999f)); }
zpl_f32 zpl_ceil(zpl_f32 x)  { return (float)((x <  0.0f) ? (int)x              : ((int)x) + 1); }

zpl_f32 zpl_half_to_float(zpl_half value) {
    union {
        unsigned int i;
        zpl_f32 f;
    } result;
    int s = (value >> 15) & 0x001;
    int e = (value >> 10) & 0x01f;
    int m = value & 0x3ff;

    if (e == 0) {
        if (m == 0) {
            /* Plus or minus zero */
            result.i = (unsigned int)(s << 31);
            return result.f;
        } else {
            /* Denormalized number */
            while (!(m & 0x00000400)) {
                m <<= 1;
                e -= 1;
            }

            e += 1;
            m &= ~0x00000400;
        }
    } else if (e == 31) {
        if (m == 0) {
            /* Positive or negative infinity */
            result.i = (unsigned int)((s << 31) | 0x7f800000);
            return result.f;
        } else {
            /* Nan */
            result.i = (unsigned int)((s << 31) | 0x7f800000 | (m << 13));
            return result.f;
        }
    }

    e = e + (127 - 15);
    m = m << 13;

    result.i = (unsigned int)((s << 31) | (e << 23) | m);
    return result.f;
}

zpl_half zpl_float_to_half(zpl_f32 value) {
    union {
        unsigned int i;
        zpl_f32 f;
    } v;
    int i, s, e, m;

    v.f = value;
    i = (int)v.i;

    s = (i >> 16) & 0x00008000;
    e = ((i >> 23) & 0x000000ff) - (127 - 15);
    m = i & 0x007fffff;

    if (e <= 0) {
        if (e < -10) return (zpl_half)s;
        m = (m | 0x00800000) >> (1 - e);

        if (m & 0x00001000) m += 0x00002000;

        return (zpl_half)(s | (m >> 13));
    } else if (e == 0xff - (127 - 15)) {
        if (m == 0) {
            return (zpl_half)(s | 0x7c00); /* NOTE: infinity */
        } else {
            /* NOTE: NAN */
            m >>= 13;
            return (zpl_half)(s | 0x7c00 | m | (m == 0));
        }
    } else {
        if (m & 0x00001000) {
            m += 0x00002000;
            if (m & 0x00800000) {
                m = 0;
                e += 1;
            }
        }

        if (e > 30) {
            zpl_f32 volatile f = 1e12f;
            int j;
            for (j = 0; j < 10; j++) f *= f; /* NOTE: Cause overflow */

            return (zpl_half)(s | 0x7c00);
        }

        return (zpl_half)(s | (e << 10) | (m >> 13));
    }
}

#define ZPL_VEC2_2OP(a, c, post) \
    a->x = c.x post;             \
    a->y = c.y post;

#define ZPL_VEC2_3OP(a, b, op, c, post) \
    a->x = b.x op c.x post;             \
    a->y = b.y op c.y post;

#define ZPL_VEC3_2OP(a, c, post) \
    a->x = c.x post;             \
    a->y = c.y post;             \
    a->z = c.z post;

#define ZPL_VEC3_3OP(a, b, op, c, post) \
    a->x = b.x op c.x post;             \
    a->y = b.y op c.y post;             \
    a->z = b.z op c.z post;

#define ZPL_VEC4_2OP(a, c, post) \
    a->x = c.x post;             \
    a->y = c.y post;             \
    a->z = c.z post;             \
    a->w = c.w post;

#define ZPL_VEC4_3OP(a, b, op, c, post) \
    a->x = b.x op c.x post;             \
    a->y = b.y op c.y post;             \
    a->z = b.z op c.z post;             \
    a->w = b.w op c.w post;

zpl_vec2 zpl_vec2f_zero(void) {
    zpl_vec2 v = { 0, 0 };
    return v;
}
zpl_vec2 zpl_vec2f(zpl_f32 x, zpl_f32 y) {
    zpl_vec2 v;
    v.x = x;
    v.y = y;
    return v;
}
zpl_vec2 zpl_vec2fv(zpl_f32 x[2]) {
    zpl_vec2 v;
    v.x = x[0];
    v.y = x[1];
    return v;
}

zpl_vec3 zpl_vec3f_zero(void) {
    zpl_vec3 v = { 0, 0, 0 };
    return v;
}
zpl_vec3 zpl_vec3f(zpl_f32 x, zpl_f32 y, zpl_f32 z) {
    zpl_vec3 v;
    v.x = x;
    v.y = y;
    v.z = z;
    return v;
}
zpl_vec3 zpl_vec3fv(zpl_f32 x[3]) {
    zpl_vec3 v;
    v.x = x[0];
    v.y = x[1];
    v.z = x[2];
    return v;
}

zpl_vec4 zpl_vec4f_zero(void) {
    zpl_vec4 v = { 0, 0, 0, 0 };
    return v;
}
zpl_vec4 zpl_vec4f(zpl_f32 x, zpl_f32 y, zpl_f32 z, zpl_f32 w) {
    zpl_vec4 v;
    v.x = x;
    v.y = y;
    v.z = z;
    v.w = w;
    return v;
}
zpl_vec4 zpl_vec4fv(zpl_f32 x[4]) {
    zpl_vec4 v;
    v.x = x[0];
    v.y = x[1];
    v.z = x[2];
    v.w = x[3];
    return v;
}

void zpl_vec2_add(zpl_vec2 *d, zpl_vec2 v0, zpl_vec2 v1) { ZPL_VEC2_3OP(d, v0, +, v1, +0); }
void zpl_vec2_sub(zpl_vec2 *d, zpl_vec2 v0, zpl_vec2 v1) { ZPL_VEC2_3OP(d, v0, -, v1, +0); }
void zpl_vec2_mul(zpl_vec2 *d, zpl_vec2 v, zpl_f32 s)    { ZPL_VEC2_2OP(d, v, *s); }
void zpl_vec2_div(zpl_vec2 *d, zpl_vec2 v, zpl_f32 s)    { ZPL_VEC2_2OP(d, v, / s); }

void zpl_vec3_add(zpl_vec3 *d, zpl_vec3 v0, zpl_vec3 v1) { ZPL_VEC3_3OP(d, v0, +, v1, +0); }
void zpl_vec3_sub(zpl_vec3 *d, zpl_vec3 v0, zpl_vec3 v1) { ZPL_VEC3_3OP(d, v0, -, v1, +0); }
void zpl_vec3_mul(zpl_vec3 *d, zpl_vec3 v, zpl_f32 s)    { ZPL_VEC3_2OP(d, v, *s); }
void zpl_vec3_div(zpl_vec3 *d, zpl_vec3 v, zpl_f32 s)    { ZPL_VEC3_2OP(d, v, / s); }

void zpl_vec4_add(zpl_vec4 *d, zpl_vec4 v0, zpl_vec4 v1) { ZPL_VEC4_3OP(d, v0, +, v1, +0); }
void zpl_vec4_sub(zpl_vec4 *d, zpl_vec4 v0, zpl_vec4 v1) { ZPL_VEC4_3OP(d, v0, -, v1, +0); }
void zpl_vec4_mul(zpl_vec4 *d, zpl_vec4 v, zpl_f32 s)    { ZPL_VEC4_2OP(d, v, *s); }
void zpl_vec4_div(zpl_vec4 *d, zpl_vec4 v, zpl_f32 s)    { ZPL_VEC4_2OP(d, v, / s); }

void zpl_vec2_addeq(zpl_vec2 *d, zpl_vec2 v) { ZPL_VEC2_3OP(d, (*d), +, v, +0); }
void zpl_vec2_subeq(zpl_vec2 *d, zpl_vec2 v) { ZPL_VEC2_3OP(d, (*d), -, v, +0); }
void zpl_vec2_muleq(zpl_vec2 *d, zpl_f32 s)  { ZPL_VEC2_2OP(d, (*d), *s); }
void zpl_vec2_diveq(zpl_vec2 *d, zpl_f32 s)  { ZPL_VEC2_2OP(d, (*d), / s); }

void zpl_vec3_addeq(zpl_vec3 *d, zpl_vec3 v) { ZPL_VEC3_3OP(d, (*d), +, v, +0); }
void zpl_vec3_subeq(zpl_vec3 *d, zpl_vec3 v) { ZPL_VEC3_3OP(d, (*d), -, v, +0); }
void zpl_vec3_muleq(zpl_vec3 *d, zpl_f32 s)  { ZPL_VEC3_2OP(d, (*d), *s); }
void zpl_vec3_diveq(zpl_vec3 *d, zpl_f32 s)  { ZPL_VEC3_2OP(d, (*d), / s); }

void zpl_vec4_addeq(zpl_vec4 *d, zpl_vec4 v) { ZPL_VEC4_3OP(d, (*d), +, v, +0); }
void zpl_vec4_subeq(zpl_vec4 *d, zpl_vec4 v) { ZPL_VEC4_3OP(d, (*d), -, v, +0); }
void zpl_vec4_muleq(zpl_vec4 *d, zpl_f32 s)  { ZPL_VEC4_2OP(d, (*d), *s); }
void zpl_vec4_diveq(zpl_vec4 *d, zpl_f32 s)  { ZPL_VEC4_2OP(d, (*d), / s); }

#undef ZPL_VEC2_2OP
#undef ZPL_VEC2_3OP
#undef ZPL_VEC3_3OP
#undef ZPL_VEC3_2OP
#undef ZPL_VEC4_2OP
#undef ZPL_VEC4_3OP

zpl_f32 zpl_vec2_dot(zpl_vec2 v0, zpl_vec2 v1) { return v0.x * v1.x + v0.y * v1.y; }
zpl_f32 zpl_vec3_dot(zpl_vec3 v0, zpl_vec3 v1) { return v0.x * v1.x + v0.y * v1.y + v0.z * v1.z; }
zpl_f32 zpl_vec4_dot(zpl_vec4 v0, zpl_vec4 v1) { return v0.x * v1.x + v0.y * v1.y + v0.z * v1.z + v0.w * v1.w; }

void zpl_vec2_cross(zpl_f32 *d, zpl_vec2 v0, zpl_vec2 v1) { *d = v0.x * v1.y - v1.x * v0.y; }
void zpl_vec3_cross(zpl_vec3 *d, zpl_vec3 v0, zpl_vec3 v1) {
    d->x = v0.y * v1.z - v0.z * v1.y;
    d->y = v0.z * v1.x - v0.x * v1.z;
    d->z = v0.x * v1.y - v0.y * v1.x;
}

zpl_f32 zpl_vec2_mag2(zpl_vec2 v) { return zpl_vec2_dot(v, v); }
zpl_f32 zpl_vec3_mag2(zpl_vec3 v) { return zpl_vec3_dot(v, v); }
zpl_f32 zpl_vec4_mag2(zpl_vec4 v) { return zpl_vec4_dot(v, v); }

/* TODO: Create custom sqrt function */
zpl_f32 zpl_vec2_mag(zpl_vec2 v) { return zpl_sqrt(zpl_vec2_dot(v, v)); }
zpl_f32 zpl_vec3_mag(zpl_vec3 v) { return zpl_sqrt(zpl_vec3_dot(v, v)); }
zpl_f32 zpl_vec4_mag(zpl_vec4 v) { return zpl_sqrt(zpl_vec4_dot(v, v)); }

void zpl_vec2_norm(zpl_vec2 *d, zpl_vec2 v) {
    zpl_f32 inv_mag = zpl_rsqrt(zpl_vec2_dot(v, v));
    zpl_vec2_mul(d, v, inv_mag);
}
void zpl_vec3_norm(zpl_vec3 *d, zpl_vec3 v) {
    zpl_f32 mag = zpl_vec3_mag(v);
    zpl_vec3_div(d, v, mag);
}
void zpl_vec4_norm(zpl_vec4 *d, zpl_vec4 v) {
    zpl_f32 mag = zpl_vec4_mag(v);
    zpl_vec4_div(d, v, mag);
}

void zpl_vec2_norm0(zpl_vec2 *d, zpl_vec2 v) {
    zpl_f32 mag = zpl_vec2_mag(v);
    if (mag > 0)
        zpl_vec2_div(d, v, mag);
    else
        *d = zpl_vec2f_zero( );
}
void zpl_vec3_norm0(zpl_vec3 *d, zpl_vec3 v) {
    zpl_f32 mag = zpl_vec3_mag(v);
    if (mag > 0)
        zpl_vec3_div(d, v, mag);
    else
        *d = zpl_vec3f_zero( );
}
void zpl_vec4_norm0(zpl_vec4 *d, zpl_vec4 v) {
    zpl_f32 mag = zpl_vec4_mag(v);
    if (mag > 0)
        zpl_vec4_div(d, v, mag);
    else
        *d = zpl_vec4f_zero( );
}

void zpl_vec2_reflect(zpl_vec2 *d, zpl_vec2 i, zpl_vec2 n) {
    zpl_vec2 b = n;
    zpl_vec2_muleq(&b, 2.0f * zpl_vec2_dot(n, i));
    zpl_vec2_sub(d, i, b);
}

void zpl_vec3_reflect(zpl_vec3 *d, zpl_vec3 i, zpl_vec3 n) {
    zpl_vec3 b = n;
    zpl_vec3_muleq(&b, 2.0f * zpl_vec3_dot(n, i));
    zpl_vec3_sub(d, i, b);
}

void zpl_vec2_refract(zpl_vec2 *d, zpl_vec2 i, zpl_vec2 n, zpl_f32 eta) {
    zpl_vec2 a, b;
    zpl_f32 dv, k;

    dv = zpl_vec2_dot(n, i);
    k = 1.0f - eta * eta * (1.0f - dv * dv);
    zpl_vec2_mul(&a, i, eta);
    zpl_vec2_mul(&b, n, eta * dv * zpl_sqrt(k));
    zpl_vec2_sub(d, a, b);
    zpl_vec2_muleq(d, (float)(k >= 0.0f));
}

void zpl_vec3_refract(zpl_vec3 *d, zpl_vec3 i, zpl_vec3 n, zpl_f32 eta) {
    zpl_vec3 a, b;
    zpl_f32 dv, k;

    dv = zpl_vec3_dot(n, i);
    k = 1.0f - eta * eta * (1.0f - dv * dv);
    zpl_vec3_mul(&a, i, eta);
    zpl_vec3_mul(&b, n, eta * dv * zpl_sqrt(k));
    zpl_vec3_sub(d, a, b);
    zpl_vec3_muleq(d, (float)(k >= 0.0f));
}

zpl_f32 zpl_vec2_aspect_ratio(zpl_vec2 v) { return (v.y < 0.0001f) ? 0.0f : v.x / v.y; }

void zpl_mat2_transpose(zpl_mat2 *m) { zpl_float22_transpose(zpl_float22_m(m)); }
void zpl_mat2_identity(zpl_mat2 *m) { zpl_float22_identity(zpl_float22_m(m)); }
void zpl_mat2_mul(zpl_mat2 *out, zpl_mat2 *m1, zpl_mat2 *m2) {
    zpl_float22_mul(zpl_float22_m(out), zpl_float22_m(m1), zpl_float22_m(m2));
}

void zpl_float22_identity(zpl_f32 m[2][2]) {
    m[0][0] = 1;
    m[0][1] = 0;
    m[1][0] = 0;
    m[1][1] = 1;
}

void zpl_mat2_mul_vec2(zpl_vec2 *out, zpl_mat2 *m, zpl_vec2 in) { zpl_float22_mul_vec2(out, zpl_float22_m(m), in); }

zpl_mat2 *zpl_mat2_v(zpl_vec2 m[2])   { return (zpl_mat2 *)m; }
zpl_mat2 *zpl_mat2_f(zpl_f32 m[2][2]) { return (zpl_mat2 *)m; }

zpl_float2 *zpl_float22_m(zpl_mat2 *m)   { return (zpl_float2 *)m; }
zpl_float2 *zpl_float22_v(zpl_vec2 m[2]) { return (zpl_float2 *)m; }
zpl_float2 *zpl_float22_4(zpl_f32 m[4])  { return (zpl_float2 *)m; }

void zpl_float22_transpose(zpl_f32 (*vec)[2]) {
    int i, j;
    for (j = 0; j < 2; j++) {
        for (i = j + 1; i < 2; i++) {
            zpl_f32 t = vec[i][j];
            vec[i][j] = vec[j][i];
            vec[j][i] = t;
        }
    }
}

void zpl_float22_mul(zpl_f32 (*out)[2], zpl_f32 (*mat1)[2], zpl_f32 (*mat2)[2]) {
    int i, j;
    zpl_f32 temp1[2][2], temp2[2][2];
    if (mat1 == out) {
        zpl__memcpy_4byte(temp1, mat1, sizeof(temp1));
        mat1 = temp1;
    }
    if (mat2 == out) {
        zpl__memcpy_4byte(temp2, mat2, sizeof(temp2));
        mat2 = temp2;
    }
    for (j = 0; j < 2; j++) {
        for (i = 0; i < 2; i++) { out[j][i] = mat1[0][i] * mat2[j][0] + mat1[1][i] * mat2[j][1]; }
    }
}

void zpl_float22_mul_vec2(zpl_vec2 *out, zpl_f32 m[2][2], zpl_vec2 v) {
    out->x = m[0][0] * v.x + m[0][1] * v.y;
    out->y = m[1][0] * v.x + m[1][1] * v.y;
}

zpl_f32 zpl_mat2_determinate(zpl_mat2 *m) {
    zpl_float2 *e = zpl_float22_m(m);
    return e[0][0] * e[1][1] - e[1][0] * e[0][1];
}

void zpl_mat2_inverse(zpl_mat2 *out, zpl_mat2 *in) {
    zpl_float2 *o = zpl_float22_m(out);
    zpl_float2 *i = zpl_float22_m(in);

    zpl_f32 ood = 1.0f / zpl_mat2_determinate(in);

    o[0][0] = +i[1][1] * ood;
    o[0][1] = -i[0][1] * ood;
    o[1][0] = -i[1][0] * ood;
    o[1][1] = +i[0][0] * ood;
}

void zpl_mat3_transpose(zpl_mat3 *m) { zpl_float33_transpose(zpl_float33_m(m)); }
void zpl_mat3_identity(zpl_mat3 *m)  { zpl_float33_identity(zpl_float33_m(m)); }

void zpl_mat3_mul(zpl_mat3 *out, zpl_mat3 *m1, zpl_mat3 *m2) {
    zpl_float33_mul(zpl_float33_m(out), zpl_float33_m(m1), zpl_float33_m(m2));
}

void zpl_float33_identity(zpl_f32 m[3][3]) {
    m[0][0] = 1;
    m[0][1] = 0;
    m[0][2] = 0;
    m[1][0] = 0;
    m[1][1] = 1;
    m[1][2] = 0;
    m[2][0] = 0;
    m[2][1] = 0;
    m[2][2] = 1;
}

void zpl_mat3_mul_vec3(zpl_vec3 *out, zpl_mat3 *m, zpl_vec3 in) { zpl_float33_mul_vec3(out, zpl_float33_m(m), in); }

zpl_mat3 *zpl_mat3_v(zpl_vec3 m[3])   { return (zpl_mat3 *)m; }
zpl_mat3 *zpl_mat3_f(zpl_f32 m[3][3]) { return (zpl_mat3 *)m; }

zpl_float3 *zpl_float33_m(zpl_mat3 *m)   { return (zpl_float3 *)m; }
zpl_float3 *zpl_float33_v(zpl_vec3 m[3]) { return (zpl_float3 *)m; }
zpl_float3 *zpl_float33_9(zpl_f32 m[9])  { return (zpl_float3 *)m; }

void zpl_float33_transpose(zpl_f32 (*vec)[3]) {
    int i, j;
    for (j = 0; j < 3; j++) {
        for (i = j + 1; i < 3; i++) {
            zpl_f32 t = vec[i][j];
            vec[i][j] = vec[j][i];
            vec[j][i] = t;
        }
    }
}

void zpl_float33_mul(zpl_f32 (*out)[3], zpl_f32 (*mat1)[3], zpl_f32 (*mat2)[3]) {
    int i, j;
    zpl_f32 temp1[3][3], temp2[3][3];
    if (mat1 == out) {
        zpl__memcpy_4byte(temp1, mat1, sizeof(temp1));
        mat1 = temp1;
    }
    if (mat2 == out) {
        zpl__memcpy_4byte(temp2, mat2, sizeof(temp2));
        mat2 = temp2;
    }
    for (j = 0; j < 3; j++) {
        for (i = 0; i < 3; i++) {
            out[j][i] = mat1[0][i] * mat2[j][0] + mat1[1][i] * mat2[j][1] + mat1[2][i] * mat2[j][2];
        }
    }
}

void zpl_float33_mul_vec3(zpl_vec3 *out, zpl_f32 m[3][3], zpl_vec3 v) {
    out->x = m[0][0] * v.x + m[0][1] * v.y + m[0][2] * v.z;
    out->y = m[1][0] * v.x + m[1][1] * v.y + m[1][2] * v.z;
    out->z = m[2][0] * v.x + m[2][1] * v.y + m[2][2] * v.z;
}

zpl_f32 zpl_mat3_determinate(zpl_mat3 *m) {
    zpl_float3 *e = zpl_float33_m(m);
    zpl_f32 d =
        +e[0][0] * (e[1][1] * e[2][2] - e[1][2] * e[2][1])
        -e[0][1] * (e[1][0] * e[2][2] - e[1][2] * e[2][0])
        +e[0][2] * (e[1][0] * e[2][1] - e[1][1] * e[2][0]);
    return d;
}

void zpl_mat3_inverse(zpl_mat3 *out, zpl_mat3 *in) {
    zpl_float3 *o = zpl_float33_m(out);
    zpl_float3 *i = zpl_float33_m(in);

    zpl_f32 ood = 1.0f / zpl_mat3_determinate(in);

    o[0][0] = +(i[1][1] * i[2][2] - i[2][1] * i[1][2]) * ood;
    o[0][1] = -(i[1][0] * i[2][2] - i[2][0] * i[1][2]) * ood;
    o[0][2] = +(i[1][0] * i[2][1] - i[2][0] * i[1][1]) * ood;
    o[1][0] = -(i[0][1] * i[2][2] - i[2][1] * i[0][2]) * ood;
    o[1][1] = +(i[0][0] * i[2][2] - i[2][0] * i[0][2]) * ood;
    o[1][2] = -(i[0][0] * i[2][1] - i[2][0] * i[0][1]) * ood;
    o[2][0] = +(i[0][1] * i[1][2] - i[1][1] * i[0][2]) * ood;
    o[2][1] = -(i[0][0] * i[1][2] - i[1][0] * i[0][2]) * ood;
    o[2][2] = +(i[0][0] * i[1][1] - i[1][0] * i[0][1]) * ood;
}

void zpl_mat4_transpose(zpl_mat4 *m) { zpl_float44_transpose(zpl_float44_m(m)); }
void zpl_mat4_identity(zpl_mat4 *m)  { zpl_float44_identity(zpl_float44_m(m)); }

void zpl_mat4_mul(zpl_mat4 *out, zpl_mat4 *m1, zpl_mat4 *m2) {
    zpl_float44_mul(zpl_float44_m(out), zpl_float44_m(m1), zpl_float44_m(m2));
}

void zpl_float44_identity(zpl_f32 m[4][4]) {
    m[0][0] = 1;
    m[0][1] = 0;
    m[0][2] = 0;
    m[0][3] = 0;
    m[1][0] = 0;
    m[1][1] = 1;
    m[1][2] = 0;
    m[1][3] = 0;
    m[2][0] = 0;
    m[2][1] = 0;
    m[2][2] = 1;
    m[2][3] = 0;
    m[3][0] = 0;
    m[3][1] = 0;
    m[3][2] = 0;
    m[3][3] = 1;
}

void zpl_mat4_mul_vec4(zpl_vec4 *out, zpl_mat4 *m, zpl_vec4 in) { zpl_float44_mul_vec4(out, zpl_float44_m(m), in); }

zpl_mat4 *zpl_mat4_v(zpl_vec4 m[4])   { return (zpl_mat4 *)m; }
zpl_mat4 *zpl_mat4_f(zpl_f32 m[4][4]) { return (zpl_mat4 *)m; }

zpl_float4 *zpl_float44_m(zpl_mat4 *m)    { return (zpl_float4 *)m; }
zpl_float4 *zpl_float44_v(zpl_vec4 m[4])  { return (zpl_float4 *)m; }
zpl_float4 *zpl_float44_16(zpl_f32 m[16]) { return (zpl_float4 *)m; }

void zpl_float44_transpose(zpl_f32 (*vec)[4]) {
    zpl_f32 tmp;
    tmp = vec[1][0];
    vec[1][0] = vec[0][1];
    vec[0][1] = tmp;
    tmp = vec[2][0];
    vec[2][0] = vec[0][2];
    vec[0][2] = tmp;
    tmp = vec[3][0];
    vec[3][0] = vec[0][3];
    vec[0][3] = tmp;
    tmp = vec[2][1];
    vec[2][1] = vec[1][2];
    vec[1][2] = tmp;
    tmp = vec[3][1];
    vec[3][1] = vec[1][3];
    vec[1][3] = tmp;
    tmp = vec[3][2];
    vec[3][2] = vec[2][3];
    vec[2][3] = tmp;
}

void zpl_float44_mul(zpl_f32 (*out)[4], zpl_f32 (*mat1)[4], zpl_f32 (*mat2)[4]) {
    int i, j;
    zpl_f32 temp1[4][4], temp2[4][4];
    if (mat1 == out) {
        zpl__memcpy_4byte(temp1, mat1, sizeof(temp1));
        mat1 = temp1;
    }
    if (mat2 == out) {
        zpl__memcpy_4byte(temp2, mat2, sizeof(temp2));
        mat2 = temp2;
    }
    for (j = 0; j < 4; j++) {
        for (i = 0; i < 4; i++) {
            out[j][i] =
                mat1[0][i] * mat2[j][0] + mat1[1][i] * mat2[j][1]
               +mat1[2][i] * mat2[j][2] + mat1[3][i] * mat2[j][3];
        }
    }
}

void zpl_float44_mul_vec4(zpl_vec4 *out, zpl_f32 m[4][4], zpl_vec4 v) {
    out->x = m[0][0] * v.x + m[1][0] * v.y + m[2][0] * v.z + m[3][0] * v.w;
    out->y = m[0][1] * v.x + m[1][1] * v.y + m[2][1] * v.z + m[3][1] * v.w;
    out->z = m[0][2] * v.x + m[1][2] * v.y + m[2][2] * v.z + m[3][2] * v.w;
    out->w = m[0][3] * v.x + m[1][3] * v.y + m[2][3] * v.z + m[3][3] * v.w;
}

void zpl_mat4_inverse(zpl_mat4 *out, zpl_mat4 *in) {
    zpl_float4 *o = zpl_float44_m(out);
    zpl_float4 *m = zpl_float44_m(in);

    zpl_f32 ood;

    zpl_f32 sf00 = m[2][2] * m[3][3] - m[3][2] * m[2][3];
    zpl_f32 sf01 = m[2][1] * m[3][3] - m[3][1] * m[2][3];
    zpl_f32 sf02 = m[2][1] * m[3][2] - m[3][1] * m[2][2];
    zpl_f32 sf03 = m[2][0] * m[3][3] - m[3][0] * m[2][3];
    zpl_f32 sf04 = m[2][0] * m[3][2] - m[3][0] * m[2][2];
    zpl_f32 sf05 = m[2][0] * m[3][1] - m[3][0] * m[2][1];
    zpl_f32 sf06 = m[1][2] * m[3][3] - m[3][2] * m[1][3];
    zpl_f32 sf07 = m[1][1] * m[3][3] - m[3][1] * m[1][3];
    zpl_f32 sf08 = m[1][1] * m[3][2] - m[3][1] * m[1][2];
    zpl_f32 sf09 = m[1][0] * m[3][3] - m[3][0] * m[1][3];
    zpl_f32 sf10 = m[1][0] * m[3][2] - m[3][0] * m[1][2];
    zpl_f32 sf11 = m[1][1] * m[3][3] - m[3][1] * m[1][3];
    zpl_f32 sf12 = m[1][0] * m[3][1] - m[3][0] * m[1][1];
    zpl_f32 sf13 = m[1][2] * m[2][3] - m[2][2] * m[1][3];
    zpl_f32 sf14 = m[1][1] * m[2][3] - m[2][1] * m[1][3];
    zpl_f32 sf15 = m[1][1] * m[2][2] - m[2][1] * m[1][2];
    zpl_f32 sf16 = m[1][0] * m[2][3] - m[2][0] * m[1][3];
    zpl_f32 sf17 = m[1][0] * m[2][2] - m[2][0] * m[1][2];
    zpl_f32 sf18 = m[1][0] * m[2][1] - m[2][0] * m[1][1];

    o[0][0] = +(m[1][1] * sf00 - m[1][2] * sf01 + m[1][3] * sf02);
    o[1][0] = -(m[1][0] * sf00 - m[1][2] * sf03 + m[1][3] * sf04);
    o[2][0] = +(m[1][0] * sf01 - m[1][1] * sf03 + m[1][3] * sf05);
    o[3][0] = -(m[1][0] * sf02 - m[1][1] * sf04 + m[1][2] * sf05);

    o[0][1] = -(m[0][1] * sf00 - m[0][2] * sf01 + m[0][3] * sf02);
    o[1][1] = +(m[0][0] * sf00 - m[0][2] * sf03 + m[0][3] * sf04);
    o[2][1] = -(m[0][0] * sf01 - m[0][1] * sf03 + m[0][3] * sf05);
    o[3][1] = +(m[0][0] * sf02 - m[0][1] * sf04 + m[0][2] * sf05);

    o[0][2] = +(m[0][1] * sf06 - m[0][2] * sf07 + m[0][3] * sf08);
    o[1][2] = -(m[0][0] * sf06 - m[0][2] * sf09 + m[0][3] * sf10);
    o[2][2] = +(m[0][0] * sf11 - m[0][1] * sf09 + m[0][3] * sf12);
    o[3][2] = -(m[0][0] * sf08 - m[0][1] * sf10 + m[0][2] * sf12);

    o[0][3] = -(m[0][1] * sf13 - m[0][2] * sf14 + m[0][3] * sf15);
    o[1][3] = +(m[0][0] * sf13 - m[0][2] * sf16 + m[0][3] * sf17);
    o[2][3] = -(m[0][0] * sf14 - m[0][1] * sf16 + m[0][3] * sf18);
    o[3][3] = +(m[0][0] * sf15 - m[0][1] * sf17 + m[0][2] * sf18);

    ood = 1.0f / (m[0][0] * o[0][0] + m[0][1] * o[1][0] + m[0][2] * o[2][0] + m[0][3] * o[3][0]);

    o[0][0] *= ood; o[1][0] *= ood; o[2][0] *= ood; o[3][0] *= ood;
    o[0][1] *= ood; o[1][1] *= ood; o[2][1] *= ood; o[3][1] *= ood;
    o[0][2] *= ood; o[1][2] *= ood; o[2][2] *= ood; o[3][2] *= ood;
    o[0][3] *= ood; o[1][3] *= ood; o[2][3] *= ood; o[3][3] *= ood;
}

void zpl_mat4_translate(zpl_mat4 *out, zpl_vec3 v) {
    zpl_mat4_identity(out);
    out->col[3].xyz = v;
    out->col[3].w = 1;
}

void zpl_mat4_rotate(zpl_mat4 *out, zpl_vec3 v, zpl_f32 angle_radians) {
    zpl_f32 c, s;
    zpl_vec3 axis, t;
    zpl_float4 *rot;

    c = zpl_cos(angle_radians);
    s = zpl_sin(angle_radians);

    zpl_vec3_norm(&axis, v);
    zpl_vec3_mul(&t, axis, 1.0f - c);

    zpl_mat4_identity(out);
    rot = zpl_float44_m(out);

    rot[0][0] = c + t.x * axis.x;
    rot[0][1] = 0 + t.x * axis.y + s * axis.z;
    rot[0][2] = 0 + t.x * axis.z - s * axis.y;
    rot[0][3] = 0;

    rot[1][0] = 0 + t.y * axis.x - s * axis.z;
    rot[1][1] = c + t.y * axis.y;
    rot[1][2] = 0 + t.y * axis.z + s * axis.x;
    rot[1][3] = 0;

    rot[2][0] = 0 + t.z * axis.x + s * axis.y;
    rot[2][1] = 0 + t.z * axis.y - s * axis.x;
    rot[2][2] = c + t.z * axis.z;
    rot[2][3] = 0;
}

void zpl_mat4_scale(zpl_mat4 *out, zpl_vec3 v) {
    zpl_mat4_identity(out);
    out->e[0]  = v.x;
    out->e[5]  = v.y;
    out->e[10] = v.z;
}

void zpl_mat4_scalef(zpl_mat4 *out, zpl_f32 s) {
    zpl_mat4_identity(out);
    out->e[0]  = s;
    out->e[5]  = s;
    out->e[10] = s;
}

void zpl_mat4_ortho2d(zpl_mat4 *out, zpl_f32 left, zpl_f32 right, zpl_f32 bottom, zpl_f32 top) {
    zpl_float4 *m;
    zpl_mat4_identity(out);
    m = zpl_float44_m(out);

    m[0][0] = 2.0f / (right - left);
    m[1][1] = 2.0f / (top - bottom);
    m[2][2] = -1.0f;
    m[3][0] = -(right + left) / (right - left);
    m[3][1] = -(top + bottom) / (top - bottom);
}

void zpl_mat4_ortho3d(zpl_mat4 *out, zpl_f32 left, zpl_f32 right, zpl_f32 bottom, zpl_f32 top, zpl_f32 z_near, zpl_f32 z_far) {
    zpl_float4 *m;
    zpl_mat4_identity(out);
    m = zpl_float44_m(out);

    m[0][0] = +2.0f / (right - left);
    m[1][1] = +2.0f / (top - bottom);
    m[2][2] = -2.0f / (z_far - z_near);
    m[3][0] = -(right + left) / (right - left);
    m[3][1] = -(top + bottom) / (top - bottom);
    m[3][2] = -(z_far + z_near) / (z_far - z_near);
}

void zpl_mat4_perspective(zpl_mat4 *out, zpl_f32 fovy, zpl_f32 aspect, zpl_f32 z_near, zpl_f32 z_far) {
    zpl_f32 tan_half_fovy = zpl_tan(0.5f * fovy);
    zpl_mat4 zero_mat = { 0 };
    zpl_float4 *m = zpl_float44_m(out);
    *out = zero_mat;

    m[0][0] = 1.0f / (aspect * tan_half_fovy);
    m[1][1] = 1.0f / (tan_half_fovy);
    m[2][2] = -(z_far + z_near) / (z_far - z_near);
    m[2][3] = -1.0f;
    m[3][2] = -2.0f * z_far * z_near / (z_far - z_near);
}

void zpl_mat4_infinite_perspective(zpl_mat4 *out, zpl_f32 fovy, zpl_f32 aspect, zpl_f32 z_near) {
    zpl_f32 range = zpl_tan(0.5f * fovy) * z_near;
    zpl_f32 left = -range * aspect;
    zpl_f32 right = range * aspect;
    zpl_f32 bottom = -range;
    zpl_f32 top = range;
    zpl_mat4 zero_mat = { 0 };
    zpl_float4 *m = zpl_float44_m(out);
    *out = zero_mat;

    m[0][0] = (2.0f * z_near) / (right - left);
    m[1][1] = (2.0f * z_near) / (top - bottom);
    m[2][2] = -1.0f;
    m[2][3] = -1.0f;
    m[3][2] = -2.0f * z_near;
}

void zpl_mat4_look_at(zpl_mat4 *out, zpl_vec3 eye, zpl_vec3 centre, zpl_vec3 up) {
    zpl_vec3 f, s, u;
    zpl_float4 *m;

    zpl_vec3_sub(&f, centre, eye);
    zpl_vec3_norm(&f, f);

    zpl_vec3_cross(&s, f, up);
    zpl_vec3_norm(&s, s);

    zpl_vec3_cross(&u, s, f);

    zpl_mat4_identity(out);
    m = zpl_float44_m(out);

    m[0][0] = +s.x;
    m[1][0] = +s.y;
    m[2][0] = +s.z;

    m[0][1] = +u.x;
    m[1][1] = +u.y;
    m[2][1] = +u.z;

    m[0][2] = -f.x;
    m[1][2] = -f.y;
    m[2][2] = -f.z;

    m[3][0] = -zpl_vec3_dot(s, eye);
    m[3][1] = -zpl_vec3_dot(u, eye);
    m[3][2] = +zpl_vec3_dot(f, eye);
}

zpl_quat zpl_quatf(zpl_f32 x, zpl_f32 y, zpl_f32 z, zpl_f32 w) {
    zpl_quat q;
    q.x = x;
    q.y = y;
    q.z = z;
    q.w = w;
    return q;
}
zpl_quat zpl_quatfv(zpl_f32 e[4]) {
    zpl_quat q;
    q.x = e[0];
    q.y = e[1];
    q.z = e[2];
    q.w = e[3];
    return q;
}

zpl_quat zpl_quat_axis_angle(zpl_vec3 axis, zpl_f32 angle_radians) {
    zpl_quat q;
    zpl_vec3_norm(&q.xyz, axis);
    zpl_vec3_muleq(&q.xyz, zpl_sin(0.5f * angle_radians));
    q.w = zpl_cos(0.5f * angle_radians);
    return q;
}

zpl_quat zpl_quat_euler_angles(zpl_f32 pitch, zpl_f32 yaw, zpl_f32 roll) {
    /* TODO: Do without multiplication, i.e. make it faster */
    zpl_quat q, p, y, r;
    p = zpl_quat_axis_angle(zpl_vec3f(1, 0, 0), pitch);
    y = zpl_quat_axis_angle(zpl_vec3f(0, 1, 0), yaw);
    r = zpl_quat_axis_angle(zpl_vec3f(0, 0, 1), roll);

    zpl_quat_mul(&q, y, p);
    zpl_quat_muleq(&q, r);

    return q;
}

zpl_quat zpl_quat_identity(void) {
    zpl_quat q = { 0, 0, 0, 1 };
    return q;
}

void zpl_quat_add(zpl_quat *d, zpl_quat q0, zpl_quat q1) { zpl_vec4_add(&d->xyzw, q0.xyzw, q1.xyzw); }
void zpl_quat_sub(zpl_quat *d, zpl_quat q0, zpl_quat q1) { zpl_vec4_sub(&d->xyzw, q0.xyzw, q1.xyzw); }

void zpl_quat_mul(zpl_quat *d, zpl_quat q0, zpl_quat q1) {
    d->x = q0.w * q1.x + q0.x * q1.w + q0.y * q1.z - q0.z * q1.y;
    d->y = q0.w * q1.y - q0.x * q1.z + q0.y * q1.w + q0.z * q1.x;
    d->z = q0.w * q1.z + q0.x * q1.y - q0.y * q1.x + q0.z * q1.w;
    d->w = q0.w * q1.w - q0.x * q1.x - q0.y * q1.y - q0.z * q1.z;
}

void zpl_quat_div(zpl_quat *d, zpl_quat q0, zpl_quat q1) {
    zpl_quat iq1;
    zpl_quat_inverse(&iq1, q1);
    zpl_quat_mul(d, q0, iq1);
}

void zpl_quat_mulf(zpl_quat *d, zpl_quat q0, zpl_f32 s) { zpl_vec4_mul(&d->xyzw, q0.xyzw, s); }
void zpl_quat_divf(zpl_quat *d, zpl_quat q0, zpl_f32 s) { zpl_vec4_div(&d->xyzw, q0.xyzw, s); }

void zpl_quat_addeq(zpl_quat *d, zpl_quat q) { zpl_vec4_addeq(&d->xyzw, q.xyzw); }
void zpl_quat_subeq(zpl_quat *d, zpl_quat q) { zpl_vec4_subeq(&d->xyzw, q.xyzw); }
void zpl_quat_muleq(zpl_quat *d, zpl_quat q) { zpl_quat_mul(d, *d, q); }
void zpl_quat_diveq(zpl_quat *d, zpl_quat q) { zpl_quat_div(d, *d, q); }

void zpl_quat_muleqf(zpl_quat *d, zpl_f32 s) { zpl_vec4_muleq(&d->xyzw, s); }
void zpl_quat_diveqf(zpl_quat *d, zpl_f32 s) { zpl_vec4_diveq(&d->xyzw, s); }

zpl_f32 zpl_quat_dot(zpl_quat q0, zpl_quat q1) {
    zpl_f32 r = zpl_vec3_dot(q0.xyz, q1.xyz) + q0.w * q1.w;
    return r;
}
zpl_f32 zpl_quat_mag(zpl_quat q) {
    zpl_f32 r = zpl_sqrt(zpl_quat_dot(q, q));
    return r;
}

void zpl_quat_norm(zpl_quat *d, zpl_quat q) { zpl_quat_divf(d, q, zpl_quat_mag(q)); }

void zpl_quat_conj(zpl_quat *d, zpl_quat q) {
    d->xyz = zpl_vec3f(-q.x, -q.y, -q.z);
    d->w = q.w;
}
void zpl_quat_inverse(zpl_quat *d, zpl_quat q) {
    zpl_quat_conj(d, q);
    zpl_quat_diveqf(d, zpl_quat_dot(q, q));
}

void zpl_quat_axis(zpl_vec3 *axis, zpl_quat q) {
    zpl_quat n;
    zpl_quat_norm(&n, q);
    zpl_vec3_div(axis, n.xyz, zpl_sin(zpl_arccos(q.w)));
}

zpl_f32 zpl_quat_angle(zpl_quat q) {
    zpl_f32 mag   = zpl_quat_mag(q);
    zpl_f32 c     = q.w * (1.0f / mag);
    zpl_f32 angle = 2.0f * zpl_arccos(c);
    return angle;
}

zpl_f32 zpl_quat_roll(zpl_quat q) {
    return zpl_arctan2(2.0f * q.x * q.y + q.z * q.w, q.x * q.x + q.w * q.w - q.y * q.y - q.z * q.z);
}
zpl_f32 zpl_quat_pitch(zpl_quat q) {
    return zpl_arctan2(2.0f * q.y * q.z + q.w * q.x, q.w * q.w - q.x * q.x - q.y * q.y + q.z * q.z);
}
zpl_f32 zpl_quat_yaw(zpl_quat q) { return zpl_arcsin(-2.0f * (q.x * q.z - q.w * q.y)); }

void zpl_quat_rotate_vec3(zpl_vec3 *d, zpl_quat q, zpl_vec3 v) {
    /* zpl_vec3 t = 2.0f * cross(q.xyz, v);
     * *d = q.w*t + v + cross(q.xyz, t);
     */
    zpl_vec3 t, p;
    zpl_vec3_cross(&t, q.xyz, v);
    zpl_vec3_muleq(&t, 2.0f);

    zpl_vec3_cross(&p, q.xyz, t);

    zpl_vec3_mul(d, t, q.w);
    zpl_vec3_addeq(d, v);
    zpl_vec3_addeq(d, p);
}

void zpl_mat4_from_quat(zpl_mat4 *out, zpl_quat q) {
    zpl_float4 *m;
    zpl_quat a;
    zpl_f32 xx, yy, zz, xy, xz, yz, wx, wy, wz;

    zpl_quat_norm(&a, q);
    xx = a.x * a.x;
    yy = a.y * a.y;
    zz = a.z * a.z;
    xy = a.x * a.y;
    xz = a.x * a.z;
    yz = a.y * a.z;
    wx = a.w * a.x;
    wy = a.w * a.y;
    wz = a.w * a.z;

    zpl_mat4_identity(out);
    m = zpl_float44_m(out);

    m[0][0] = 1.0f - 2.0f * (yy + zz);
    m[0][1] = 2.0f * (xy + wz);
    m[0][2] = 2.0f * (xz - wy);

    m[1][0] = 2.0f * (xy - wz);
    m[1][1] = 1.0f - 2.0f * (xx + zz);
    m[1][2] = 2.0f * (yz + wx);

    m[2][0] = 2.0f * (xz + wy);
    m[2][1] = 2.0f * (yz - wx);
    m[2][2] = 1.0f - 2.0f * (xx + yy);
}

void zpl_quat_from_mat4(zpl_quat *out, zpl_mat4 *mat) {
    zpl_float4 *m;
    zpl_f32 four_x_squared_minus_1, four_y_squared_minus_1, four_z_squared_minus_1, four_w_squared_minus_1,
    four_biggest_squared_minus_1;
    int biggest_index = 0;
    zpl_f32 biggest_value, mult;

    m = zpl_float44_m(mat);

    four_x_squared_minus_1 = m[0][0] - m[1][1] - m[2][2];
    four_y_squared_minus_1 = m[1][1] - m[0][0] - m[2][2];
    four_z_squared_minus_1 = m[2][2] - m[0][0] - m[1][1];
    four_w_squared_minus_1 = m[0][0] + m[1][1] + m[2][2];

    four_biggest_squared_minus_1 = four_w_squared_minus_1;
    if (four_x_squared_minus_1 > four_biggest_squared_minus_1) {
        four_biggest_squared_minus_1 = four_x_squared_minus_1;
        biggest_index = 1;
    }
    if (four_y_squared_minus_1 > four_biggest_squared_minus_1) {
        four_biggest_squared_minus_1 = four_y_squared_minus_1;
        biggest_index = 2;
    }
    if (four_z_squared_minus_1 > four_biggest_squared_minus_1) {
        four_biggest_squared_minus_1 = four_z_squared_minus_1;
        biggest_index = 3;
    }

    biggest_value = zpl_sqrt(four_biggest_squared_minus_1 + 1.0f) * 0.5f;
    mult = 0.25f / biggest_value;

    switch (biggest_index) {
        case 0:
            out->w = biggest_value;
            out->x = (m[1][2] - m[2][1]) * mult;
            out->y = (m[2][0] - m[0][2]) * mult;
            out->z = (m[0][1] - m[1][0]) * mult;
            break;
        case 1:
            out->w = (m[1][2] - m[2][1]) * mult;
            out->x = biggest_value;
            out->y = (m[0][1] + m[1][0]) * mult;
            out->z = (m[2][0] + m[0][2]) * mult;
            break;
        case 2:
            out->w = (m[2][0] - m[0][2]) * mult;
            out->x = (m[0][1] + m[1][0]) * mult;
            out->y = biggest_value;
            out->z = (m[1][2] + m[2][1]) * mult;
            break;
        case 3:
            out->w = (m[0][1] - m[1][0]) * mult;
            out->x = (m[2][0] + m[0][2]) * mult;
            out->y = (m[1][2] + m[2][1]) * mult;
            out->z = biggest_value;
            break;
    }
}

zpl_f32 zpl_lerp(zpl_f32 a, zpl_f32 b, zpl_f32 t) { return a * (1.0f - t) + b * t; }
zpl_f32 zpl_unlerp(zpl_f32 t, zpl_f32 a, zpl_f32 b) { return (t - a) / (b - a); }
zpl_f32 zpl_smooth_step(zpl_f32 a, zpl_f32 b, zpl_f32 t) {
    zpl_f32 x = (t - a) / (b - a);
    return x * x * (3.0f - 2.0f * x);
}
zpl_f32 zpl_smoother_step(zpl_f32 a, zpl_f32 b, zpl_f32 t) {
    zpl_f32 x = (t - a) / (b - a);
    return x * x * x * (x * (6.0f * x - 15.0f) + 10.0f);
}

#define ZPL_VEC_LERPN(N, d, a, b, t)     \
    zpl_vec##N db;                       \
    zpl_vec##N##_sub(&db, b, a);         \
    zpl_vec##N##_muleq(&db, t);          \
    zpl_vec##N##_add(d, a, db)

void zpl_vec2_lerp(zpl_vec2 *d, zpl_vec2 a, zpl_vec2 b, zpl_f32 t) { ZPL_VEC_LERPN(2, d, a, b, t); }
void zpl_vec3_lerp(zpl_vec3 *d, zpl_vec3 a, zpl_vec3 b, zpl_f32 t) { ZPL_VEC_LERPN(3, d, a, b, t); }
void zpl_vec4_lerp(zpl_vec4 *d, zpl_vec4 a, zpl_vec4 b, zpl_f32 t) { ZPL_VEC_LERPN(4, d, a, b, t); }

#undef ZPL_VEC_LERPN

void zpl_vec2_cslerp(zpl_vec2 *d, zpl_vec2 a, zpl_vec2 v0, zpl_vec2 b, zpl_vec2 v1, zpl_f32 t) {
    zpl_f32 t2 = t * t;
    zpl_f32 ti = (t - 1);
    zpl_f32 ti2 = ti * ti;

    zpl_f32 h00 = (1 + 2 * t) * ti2;
    zpl_f32 h10 = t * ti2;
    zpl_f32 h01 = t2 * (3 - 2 * t);
    zpl_f32 h11 = t2 * ti;

    d->x = h00 * a.x + h10 * v0.x + h01 * b.x + h11 * v1.x;
    d->y = h00 * a.y + h10 * v0.y + h01 * b.y + h11 * v1.y;
}

void zpl_vec3_cslerp(zpl_vec3 *d, zpl_vec3 a, zpl_vec3 v0, zpl_vec3 b, zpl_vec3 v1, zpl_f32 t) {
    zpl_f32 t2 = t * t;
    zpl_f32 ti = (t - 1);
    zpl_f32 ti2 = ti * ti;

    zpl_f32 h00 = (1 + 2 * t) * ti2;
    zpl_f32 h10 = t * ti2;
    zpl_f32 h01 = t2 * (3 - 2 * t);
    zpl_f32 h11 = t2 * ti;

    d->x = h00 * a.x + h10 * v0.x + h01 * b.x + h11 * v1.x;
    d->y = h00 * a.y + h10 * v0.y + h01 * b.y + h11 * v1.y;
    d->z = h00 * a.z + h10 * v0.z + h01 * b.z + h11 * v1.z;
}

void zpl_vec2_dcslerp(zpl_vec2 *d, zpl_vec2 a, zpl_vec2 v0, zpl_vec2 b, zpl_vec2 v1, zpl_f32 t) {
    zpl_f32 t2 = t * t;

    zpl_f32 dh00 = 6 * t2 - 6 * t;
    zpl_f32 dh10 = 3 * t2 - 4 * t + 1;
    zpl_f32 dh01 = -6 * t2 + 6 * t;
    zpl_f32 dh11 = 3 * t2 - 2 * t;

    d->x = dh00 * a.x + dh10 * v0.x + dh01 * b.x + dh11 * v1.x;
    d->y = dh00 * a.y + dh10 * v0.y + dh01 * b.y + dh11 * v1.y;
}

void zpl_vec3_dcslerp(zpl_vec3 *d, zpl_vec3 a, zpl_vec3 v0, zpl_vec3 b, zpl_vec3 v1, zpl_f32 t) {
    zpl_f32 t2 = t * t;

    zpl_f32 dh00 = 6 * t2 - 6 * t;
    zpl_f32 dh10 = 3 * t2 - 4 * t + 1;
    zpl_f32 dh01 = -6 * t2 + 6 * t;
    zpl_f32 dh11 = 3 * t2 - 2 * t;

    d->x = dh00 * a.x + dh10 * v0.x + dh01 * b.x + dh11 * v1.x;
    d->y = dh00 * a.y + dh10 * v0.y + dh01 * b.y + dh11 * v1.y;
    d->z = dh00 * a.z + dh10 * v0.z + dh01 * b.z + dh11 * v1.z;
}

void zpl_quat_lerp(zpl_quat *d, zpl_quat a, zpl_quat b, zpl_f32 t) { zpl_vec4_lerp(&d->xyzw, a.xyzw, b.xyzw, t); }
void zpl_quat_nlerp(zpl_quat *d, zpl_quat a, zpl_quat b, zpl_f32 t) {
    zpl_quat_lerp(d, a, b, t);
    zpl_quat_norm(d, *d);
}

void zpl_quat_slerp(zpl_quat *d, zpl_quat a, zpl_quat b, zpl_f32 t) {
    zpl_quat x, y, z;
    zpl_f32 cos_theta, angle;
    zpl_f32 s1, s0, is;

    z = b;
    cos_theta = zpl_quat_dot(a, b);

    if (cos_theta < 0.0f) {
        z = zpl_quatf(-b.x, -b.y, -b.z, -b.w);
        cos_theta = -cos_theta;
    }

    if (cos_theta > 1.0f) {
        /* NOTE: Use lerp not nlerp as it's not a real angle or they are not normalized */
        zpl_quat_lerp(d, a, b, t);
    }

    angle = zpl_arccos(cos_theta);

    s1 = zpl_sin((1.0f - t) * angle);
    s0 = zpl_sin(t * angle);
    is = 1.0f / zpl_sin(angle);
    zpl_quat_mulf(&x, a, s1);
    zpl_quat_mulf(&y, z, s0);
    zpl_quat_add(d, x, y);
    zpl_quat_muleqf(d, is);
}

void zpl_quat_slerp_approx(zpl_quat *d, zpl_quat a, zpl_quat b, zpl_f32 t) {
    /* NOTE: Derived by taylor expanding the geometric interpolation equation
     *             Even works okay for nearly anti-parallel versors!!!
     */
    /* NOTE: Extra interations cannot be used as they require angle^4 which is not worth it to approximate */
    zpl_f32 tp = t + (1.0f - zpl_quat_dot(a, b)) / 3.0f * t * (-2.0f * t * t + 3.0f * t - 1.0f);
    zpl_quat_nlerp(d, a, b, tp);
}

void zpl_quat_nquad(zpl_quat *d, zpl_quat p, zpl_quat a, zpl_quat b, zpl_quat q, zpl_f32 t) {
    zpl_quat x, y;
    zpl_quat_nlerp(&x, p, q, t);
    zpl_quat_nlerp(&y, a, b, t);
    zpl_quat_nlerp(d, x, y, 2.0f * t * (1.0f - t));
}

void zpl_quat_squad(zpl_quat *d, zpl_quat p, zpl_quat a, zpl_quat b, zpl_quat q, zpl_f32 t) {
    zpl_quat x, y;
    zpl_quat_slerp(&x, p, q, t);
    zpl_quat_slerp(&y, a, b, t);
    zpl_quat_slerp(d, x, y, 2.0f * t * (1.0f - t));
}

void zpl_quat_squad_approx(zpl_quat *d, zpl_quat p, zpl_quat a, zpl_quat b, zpl_quat q, zpl_f32 t) {
    zpl_quat x, y;
    zpl_quat_slerp_approx(&x, p, q, t);
    zpl_quat_slerp_approx(&y, a, b, t);
    zpl_quat_slerp_approx(d, x, y, 2.0f * t * (1.0f - t));
}

zpl_rect2 zpl_rect2f(zpl_vec2 pos, zpl_vec2 dim) {
    zpl_rect2 r;
    r.pos = pos;
    r.dim = dim;
    return r;
}

zpl_rect3 zpl_rect3f(zpl_vec3 pos, zpl_vec3 dim) {
    zpl_rect3 r;
    r.pos = pos;
    r.dim = dim;
    return r;
}

int zpl_rect2_contains(zpl_rect2 a, zpl_f32 x, zpl_f32 y) {
    zpl_f32 min_x = zpl_min(a.pos.x, a.pos.x + a.dim.x);
    zpl_f32 max_x = zpl_max(a.pos.x, a.pos.x + a.dim.x);
    zpl_f32 min_y = zpl_min(a.pos.y, a.pos.y + a.dim.y);
    zpl_f32 max_y = zpl_max(a.pos.y, a.pos.y + a.dim.y);
    int result    =        (x >= min_x) & (x < max_x) & (y >= min_y) & (y < max_y);
    return result;
}

int zpl_rect2_contains_vec2(zpl_rect2 a, zpl_vec2 p) { return zpl_rect2_contains(a, p.x, p.y); }

int zpl_rect2_intersects(zpl_rect2 a, zpl_rect2 b) {
    zpl_rect2 r = { 0 };
    return zpl_rect2_intersection_result(a, b, &r);
}

int zpl_rect2_intersection_result(zpl_rect2 a, zpl_rect2 b, zpl_rect2 *intersection) {
    zpl_f32 a_min_x = zpl_min(a.pos.x, a.pos.x + a.dim.x);
    zpl_f32 a_max_x = zpl_max(a.pos.x, a.pos.x + a.dim.x);
    zpl_f32 a_min_y = zpl_min(a.pos.y, a.pos.y + a.dim.y);
    zpl_f32 a_max_y = zpl_max(a.pos.y, a.pos.y + a.dim.y);

    zpl_f32 b_min_x = zpl_min(b.pos.x, b.pos.x + b.dim.x);
    zpl_f32 b_max_x = zpl_max(b.pos.x, b.pos.x + b.dim.x);
    zpl_f32 b_min_y = zpl_min(b.pos.y, b.pos.y + b.dim.y);
    zpl_f32 b_max_y = zpl_max(b.pos.y, b.pos.y + b.dim.y);

    zpl_f32 x0 = zpl_max(a_min_x, b_min_x);
    zpl_f32 y0 = zpl_max(a_min_y, b_min_y);
    zpl_f32 x1 = zpl_min(a_max_x, b_max_x);
    zpl_f32 y1 = zpl_min(a_max_y, b_max_y);

    if ((x0 < x1) && (y0 < y1)) {
        zpl_rect2 r = zpl_rect2f(zpl_vec2f(x0, y0), zpl_vec2f(x1 - x0, y1 - y0));
        *intersection = r;
        return 1;
    } else {
        zpl_rect2 r = { 0 };
        *intersection = r;
        return 0;
    }
}

ZPL_END_C_DECLS
