#undef UFBXT_TEST_GROUP
#define UFBXT_TEST_GROUP "math"

#if UFBXT_IMPL

static ufbx_real ufbxt_quat_error(ufbx_quat a, ufbx_quat b)
{
	double pos = fabs(a.x-b.x) + fabs(a.y-b.y) + fabs(a.z-b.z) + fabs(a.w-b.w);
	double neg = fabs(a.x+b.x) + fabs(a.y+b.y) + fabs(a.z+b.z) + fabs(a.w+b.w);
	return pos < neg ? pos : neg;
}

static uint32_t ufbxt_xorshift32(uint32_t *state)
{
	/* Algorithm "xor" from p. 4 of Marsaglia, "Xorshift RNGs" */
	uint32_t x = *state;
	x ^= x << 13;
	x ^= x >> 17;
	x ^= x << 5;
	return *state = x;
}

static ufbx_real ufbxt_xorshift32_real(uint32_t *state)
{
	uint32_t u = ufbxt_xorshift32(state);
	return (ufbx_real)u * (ufbx_real)2.3283064365386962890625e-10;
}

#endif

UFBXT_TEST(quat_to_euler_structured)
#if UFBXT_IMPL
{
	ufbxt_diff_error err = { 0 };

	for (int iorder = 0; iorder < 6; iorder++) {
		ufbx_rotation_order order = (ufbx_rotation_order)iorder;

		for (int x = -360; x <= 360; x += 45)
		for (int y = -360; y <= 360; y += 45)
		for (int z = -360; z <= 360; z += 45) {
			ufbx_vec3 v = { (ufbx_real)x, (ufbx_real)y, (ufbx_real)z };

			ufbx_quat q = ufbx_euler_to_quat(v, order);
			ufbx_vec3 v2 = ufbx_quat_to_euler(q, order);
			ufbx_quat q2 = ufbx_euler_to_quat(v2, order);

			ufbxt_assert_close_real(&err, ufbxt_quat_error(q, q2), 0.0f);
			ufbxt_assert_close_real(&err, ufbxt_quat_error(q, q2), 0.0f);
		}
	}

	ufbxt_logf(".. Absolute diff: avg %.3g, max %.3g (%zu tests)", err.sum / (ufbx_real)err.num, err.max, err.num);
}
#endif

UFBXT_TEST(quat_to_euler_random)
#if UFBXT_IMPL
{
	size_t steps = ufbxt_begin_fuzz() ? 10000000 : 100000;
	ufbxt_diff_error err = { 0 };

	for (int iorder = 0; iorder < 6; iorder++) {
		ufbx_rotation_order order = (ufbx_rotation_order)iorder;

		uint32_t state = 1;

		for (size_t i = 0; i < steps; i++) {
			if (g_fuzz && ufbxt_fuzz_should_skip((int)i >> 8)) continue;

			ufbx_quat q;
			q.x = ufbxt_xorshift32_real(&state) * 2.0f - 1.0f;
			q.y = ufbxt_xorshift32_real(&state) * 2.0f - 1.0f;
			q.z = ufbxt_xorshift32_real(&state) * 2.0f - 1.0f;
			q.w = ufbxt_xorshift32_real(&state) * 2.0f - 1.0f;
			ufbx_real qm = (ufbx_real)sqrt(q.x*q.x + q.y*q.y + q.z*q.z + q.w*q.w);
			q.x /= qm;
			q.y /= qm;
			q.z /= qm;
			q.w /= qm;

			ufbx_vec3 v = ufbx_quat_to_euler(q, order);
			ufbx_quat q2 = ufbx_euler_to_quat(v, order);

			ufbxt_assert_close_real(&err, ufbxt_quat_error(q, q2), 0.0f);
		}
	}

	ufbxt_logf(".. Absolute diff: avg %.3g, max %.3g (%zu tests)", err.sum / (ufbx_real)err.num, err.max, err.num);
}
#endif

UFBXT_TEST(matrix_to_transform_structured)
#if UFBXT_IMPL
{
	ufbxt_diff_error err = { 0 };

	for (int sx = -2; sx <= 2; sx++)
	for (int sy = -2; sy <= 2; sy++)
	for (int sz = -2; sz <= 2; sz++)
	for (int rx = -2; rx <= 2; rx++)
	for (int ry = -2; ry <= 2; ry++)
	for (int rz = -2; rz <= 2; rz++)
	for (int rw = -2; rw <= 2; rw++)
	{
		// TODO: Support single axis squish?
		if (sx == 0 || sy == 0 || sz == 0) continue;
		if (rx == 0 && ry == 0 && rz == 0 && rw == 0) continue;

		ufbx_transform t;

		ufbx_quat q = { (ufbx_real)rx / 3.0f, (ufbx_real)ry / 3.0f, (ufbx_real)rz / 3.0f, (ufbx_real)rw / 3.0f };
		ufbx_real qm = (ufbx_real)sqrt(q.x*q.x + q.y*q.y + q.z*q.z + q.w*q.w);
		t.rotation.x = q.x / qm;
		t.rotation.y = q.y / qm;
		t.rotation.z = q.z / qm;
		t.rotation.w = q.w / qm;

		t.translation.x = 1.0f;
		t.translation.y = 2.0f;
		t.translation.z = 3.0f;
		t.scale.x = (ufbx_real)sx / 2.0f;
		t.scale.y = (ufbx_real)sy / 2.0f;
		t.scale.z = (ufbx_real)sz / 2.0f;

		ufbx_matrix m = ufbx_transform_to_matrix(&t);
		ufbx_transform t2 = ufbx_matrix_to_transform(&m);

		if (sx < 0 || sy < 0 || sz < 0) {
			// Flipped signs cannot be uniquely recovered, check that the transforms are identical
			ufbx_matrix m2 = ufbx_transform_to_matrix(&t2);
			ufbxt_assert_close_matrix(&err, m, m2);
		} else {
			ufbxt_assert_close_vec3(&err, t.translation, t2.translation);
			ufbxt_assert_close_vec3(&err, t.scale, t2.scale);
			ufbxt_assert_close_real(&err, ufbxt_quat_error(t.rotation, t2.rotation), 0.0f);
		}
	}

	ufbxt_logf(".. Absolute diff: avg %.3g, max %.3g (%zu tests)", err.sum / (ufbx_real)err.num, err.max, err.num);
}
#endif

UFBXT_TEST(matrix_to_transform_random)
#if UFBXT_IMPL
{
	ufbxt_diff_error err = { 0 };

	uint32_t state = 1;
	size_t steps = ufbxt_begin_fuzz() ? 1000000 : 100000;

	for (size_t i = 0; i < steps; i++) {
		if (g_fuzz && ufbxt_fuzz_should_skip((int)i >> 4)) continue;

		ufbx_transform t;

		ufbx_quat q;
		q.x = ufbxt_xorshift32_real(&state) * 2.0f - 1.0f;
		q.y = ufbxt_xorshift32_real(&state) * 2.0f - 1.0f;
		q.z = ufbxt_xorshift32_real(&state) * 2.0f - 1.0f;
		q.w = ufbxt_xorshift32_real(&state) * 2.0f - 1.0f;
		ufbx_real qm = (ufbx_real)sqrt(q.x*q.x + q.y*q.y + q.z*q.z + q.w*q.w);
		t.rotation.x = q.x / qm;
		t.rotation.y = q.y / qm;
		t.rotation.z = q.z / qm;
		t.rotation.w = q.w / qm;

		t.translation.x = ufbxt_xorshift32_real(&state) * 20.0f - 10.0f;
		t.translation.y = ufbxt_xorshift32_real(&state) * 20.0f - 10.0f;
		t.translation.z = ufbxt_xorshift32_real(&state) * 20.0f - 10.0f;
		t.scale.x = ufbxt_xorshift32_real(&state) * 10.0f + 0.01f;
		t.scale.y = ufbxt_xorshift32_real(&state) * 10.0f + 0.01f;
		t.scale.z = ufbxt_xorshift32_real(&state) * 10.0f + 0.01f;

		uint32_t flip = ufbxt_xorshift32(&state);

		// Prevent most of the inputs being flips
		if (flip & 8) flip = 0;

		if (flip & 1) t.scale.x *= -1.0f;
		if (flip & 2) t.scale.y *= -1.0f;
		if (flip & 4) t.scale.z *= -1.0f;

		ufbx_matrix m = ufbx_transform_to_matrix(&t);
		ufbx_transform t2 = ufbx_matrix_to_transform(&m);

		if (flip) {
			// Flipped signs cannot be uniquely recovered, check that the transforms are identical
			ufbx_matrix m2 = ufbx_transform_to_matrix(&t2);
			ufbxt_assert_close_matrix(&err, m, m2);
		} else {
			ufbxt_assert_close_vec3(&err, t.translation, t2.translation);
			ufbxt_assert_close_vec3(&err, t.scale, t2.scale);
			ufbxt_assert_close_real(&err, ufbxt_quat_error(t.rotation, t2.rotation), 0.0f);
		}
	}

	ufbxt_logf(".. Absolute diff: avg %.3g, max %.3g (%zu tests)", err.sum / (ufbx_real)err.num, err.max, err.num);
}
#endif

UFBXT_TEST(matrix_inverse_simple)
#if UFBXT_IMPL
{
	ufbxt_diff_error err = { 0 };

	{
		ufbx_matrix m = { 0 };
		m.m00 = 2.0f;
		m.m11 = 0.5f;
		m.m22 = 0.25f;
		m.m03 = 1.0f;
		m.m13 = 2.0f;
		m.m23 = 3.0f;

		ufbx_matrix im = ufbx_matrix_invert(&m);
		ufbxt_assert_close_real(&err, im.m00, 0.5f);
		ufbxt_assert_close_real(&err, im.m10, 0.0f);
		ufbxt_assert_close_real(&err, im.m20, 0.0f);
		ufbxt_assert_close_real(&err, im.m01, 0.0f);
		ufbxt_assert_close_real(&err, im.m11, 2.0f);
		ufbxt_assert_close_real(&err, im.m21, 0.0f);
		ufbxt_assert_close_real(&err, im.m02, 0.0f);
		ufbxt_assert_close_real(&err, im.m12, 0.0f);
		ufbxt_assert_close_real(&err, im.m22, 4.0f);
		ufbxt_assert_close_real(&err, im.m03, -0.5f);
		ufbxt_assert_close_real(&err, im.m13, -4.0f);
		ufbxt_assert_close_real(&err, im.m23, -12.0f);
	}

	{
		ufbx_matrix m = { 0 };
		m.m00 = 1.0f;
		m.m12 = -1.0f;
		m.m21 = 1.0f;
		m.m13 = 1.0f;
		m.m23 = 2.0f;

		ufbx_matrix im = ufbx_matrix_invert(&m);
		ufbxt_assert_close_real(&err, im.m00, 1.0f);
		ufbxt_assert_close_real(&err, im.m10, 0.0f);
		ufbxt_assert_close_real(&err, im.m20, 0.0f);
		ufbxt_assert_close_real(&err, im.m01, 0.0f);
		ufbxt_assert_close_real(&err, im.m11, 0.0f);
		ufbxt_assert_close_real(&err, im.m21, -1.0f);
		ufbxt_assert_close_real(&err, im.m02, 0.0f);
		ufbxt_assert_close_real(&err, im.m12, 1.0f);
		ufbxt_assert_close_real(&err, im.m22, 0.0f);
		ufbxt_assert_close_real(&err, im.m03, 0.0f);
		ufbxt_assert_close_real(&err, im.m13, -2.0f);
		ufbxt_assert_close_real(&err, im.m23, 1.0f);
	}

	ufbxt_logf(".. Absolute diff: avg %.3g, max %.3g (%zu tests)", err.sum / (ufbx_real)err.num, err.max, err.num);
}
#endif

UFBXT_TEST(matrix_inverse_random)
#if UFBXT_IMPL
{
	ufbxt_diff_error err = { 0 };

	size_t steps = ufbxt_begin_fuzz() ? 1000000 : 10000;

	uint32_t state = 1;

	for (size_t i = 0; i < steps; i++) {
		if (g_fuzz && ufbxt_fuzz_should_skip((int)i >> 4)) continue;

		ufbx_transform t;

		ufbx_quat q;
		q.x = ufbxt_xorshift32_real(&state) * 2.0f - 1.0f;
		q.y = ufbxt_xorshift32_real(&state) * 2.0f - 1.0f;
		q.z = ufbxt_xorshift32_real(&state) * 2.0f - 1.0f;
		q.w = ufbxt_xorshift32_real(&state) * 2.0f - 1.0f;
		ufbx_real qm = (ufbx_real)sqrt(q.x*q.x + q.y*q.y + q.z*q.z + q.w*q.w);
		t.rotation.x = q.x / qm;
		t.rotation.y = q.y / qm;
		t.rotation.z = q.z / qm;
		t.rotation.w = q.w / qm;

		t.translation.x = ufbxt_xorshift32_real(&state) * 20.0f - 10.0f;
		t.translation.y = ufbxt_xorshift32_real(&state) * 20.0f - 10.0f;
		t.translation.z = ufbxt_xorshift32_real(&state) * 20.0f - 10.0f;
		t.scale.x = ufbxt_xorshift32_real(&state) * 10.0f + 0.1f;
		t.scale.y = ufbxt_xorshift32_real(&state) * 10.0f + 0.1f;
		t.scale.z = ufbxt_xorshift32_real(&state) * 10.0f + 0.1f;

		uint32_t flip = ufbxt_xorshift32(&state);

		// Prevent most of the inputs being flips
		if (flip & 8) flip = 0;

		if (flip & 1) t.scale.x *= -1.0f;
		if (flip & 2) t.scale.y *= -1.0f;
		if (flip & 4) t.scale.z *= -1.0f;

		ufbx_matrix m = ufbx_transform_to_matrix(&t);
		ufbx_matrix im = ufbx_matrix_invert(&m);
		ufbx_matrix identity = ufbx_matrix_mul(&m, &im);

		ufbxt_assert_close_real(&err, identity.m00, 1.0f);
		ufbxt_assert_close_real(&err, identity.m10, 0.0f);
		ufbxt_assert_close_real(&err, identity.m20, 0.0f);
		ufbxt_assert_close_real(&err, identity.m01, 0.0f);
		ufbxt_assert_close_real(&err, identity.m11, 1.0f);
		ufbxt_assert_close_real(&err, identity.m21, 0.0f);
		ufbxt_assert_close_real(&err, identity.m02, 0.0f);
		ufbxt_assert_close_real(&err, identity.m12, 0.0f);
		ufbxt_assert_close_real(&err, identity.m22, 1.0f);
		ufbxt_assert_close_real(&err, identity.m03, 0.0f);
		ufbxt_assert_close_real(&err, identity.m13, 0.0f);
		ufbxt_assert_close_real(&err, identity.m23, 0.0f);
	}

	ufbxt_logf(".. Absolute diff: avg %.3g, max %.3g (%zu tests)", err.sum / (ufbx_real)err.num, err.max, err.num);
}
#endif
