#include "w3cs_quality.h"

#include <assert.h>
#include <float.h>
#include <math.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>

static struct w3cs_quality_projection orthographic_projection(void)
{
    struct w3cs_quality_projection projection;
    memset(&projection, 0, sizeof(projection));
    projection.object_to_clip[0] = 0.01f;
    projection.object_to_clip[5] = 0.01f;
    projection.object_to_clip[10] = 0.01f;
    projection.object_to_clip[14] = 0.5f;
    projection.object_to_clip[15] = 1.0f;
    projection.viewport_width = 1280.0f;
    projection.viewport_height = 720.0f;
    return projection;
}

static struct w3cs_quality_projection unit_depth_projection(void)
{
    struct w3cs_quality_projection projection;
    memset(&projection, 0, sizeof(projection));
    projection.object_to_clip[0] = 0.001f;
    projection.object_to_clip[5] = 0.001f;
    projection.object_to_clip[10] = 1.0f;
    projection.object_to_clip[15] = 1.0f;
    projection.viewport_width = 1280.0f;
    projection.viewport_height = 720.0f;
    return projection;
}

static void test_profile_ladder(void)
{
    unsigned level;
    const struct w3cs_quality_profile *quality10 = w3cs_quality_profile(10);
    assert(!w3cs_quality_profile(0));
    assert(!w3cs_quality_profile(11));
    assert(quality10);
    assert(quality10->max_position_drop_bits == 0);
    assert(quality10->octa_normal_bits == 8);
    assert(quality10->half_mantissa_drop_bits == 0);
    assert(w3cs_quality_profile(1)->half_mantissa_drop_bits == 6);
    assert(fabsf(quality10->normal_hold_cos_squared - 0.9999f) < 1e-7f);
    assert(fabsf(quality10->normal_minimum_dot - 0.9998f) < 1e-7f);
    for (level = 1; level <= 10; ++level) {
        const struct w3cs_quality_profile *profile =
            w3cs_quality_profile(level);
        assert(profile->preserve_flags == 0x1fu);
        assert(profile->octa_normal_bits >= 4);
        assert(profile->octa_normal_bits <= 8);
        if (level > 1) {
            const struct w3cs_quality_profile *lower =
                w3cs_quality_profile(level - 1);
            assert(profile->max_position_drop_bits
                   <= lower->max_position_drop_bits);
            assert(profile->octa_normal_bits >= lower->octa_normal_bits);
        }
        if (level > 1)
            assert(profile->half_mantissa_drop_bits == 0);
    }
}

static void test_half_delta_precision(void)
{
    const struct w3cs_quality_profile *quality1 = w3cs_quality_profile(1);
    const struct w3cs_quality_profile *quality10 = w3cs_quality_profile(10);
    assert(w3cs_quality_coarsen_half_delta(quality1, 0x3bffu) == 0x3bc0u);
    assert(w3cs_quality_coarsen_half_delta(quality1, 0xbc3fu) == 0xbc00u);
    assert(w3cs_quality_coarsen_half_delta(quality1, 0x8001u) == 0x8000u);
    assert(w3cs_quality_coarsen_half_delta(quality10, 0x3bffu) == 0x3bffu);
    assert(w3cs_quality_coarsen_half_delta(NULL, 0x3555u) == 0x3555u);
}

static void test_uniform_texel_detection(void)
{
    const uint8_t uniform_16[] = {
        0x21, 0xc2, 0x21, 0xc2, 0x21, 0xc2, 0x21, 0xc2
    };
    const uint8_t varied_16[] = {
        0x21, 0xc2, 0x21, 0xc2, 0x20, 0xc2, 0x21, 0xc2
    };
    const uint8_t uniform_32[] = {
        0x11, 0x22, 0x33, 0xff, 0x11, 0x22, 0x33, 0xff
    };
    assert(w3cs_quality_texels_are_uniform(
        uniform_16, sizeof(uniform_16), 2u));
    assert(!w3cs_quality_texels_are_uniform(
        varied_16, sizeof(varied_16), 2u));
    assert(w3cs_quality_texels_are_uniform(
        uniform_32, sizeof(uniform_32), 4u));
    assert(!w3cs_quality_texels_are_uniform(NULL, 4u, 2u));
    assert(!w3cs_quality_texels_are_uniform(uniform_16, 0u, 2u));
    assert(!w3cs_quality_texels_are_uniform(
        uniform_16, sizeof(uniform_16) - 1u, 2u));
    assert(!w3cs_quality_texels_are_uniform(
        uniform_16, sizeof(uniform_16), 0u));
}

static void test_position_quantizer(void)
{
    int16_t wire;
    float reconstructed;
    float base = 100.0f;
    unsigned frame;
    assert(w3cs_quality_quantize_position(100.024f, base, 0,
                                          &wire, &reconstructed));
    assert(wire == 2);
    assert(reconstructed == 100.03125f);
    assert(w3cs_quality_quantize_position(100.024f, base, 2,
                                          &wire, &reconstructed));
    assert(wire == 0);
    assert(reconstructed == base);
    assert(!w3cs_quality_quantize_position(INFINITY, base, 0,
                                           &wire, &reconstructed));
    assert(!w3cs_quality_quantize_position(FLT_MAX, -FLT_MAX, 0,
                                           &wire, &reconstructed));
    base = 0.0f;
    for (frame = 1; frame <= 1000u; ++frame) {
        float current = frame * 0.01f;
        assert(w3cs_quality_quantize_position(current, base, 4,
                                              &wire, &reconstructed));
        assert((wire & 15) == 0);
        assert(fabsf(reconstructed - current) <= 0.12501f);
        base = reconstructed;
    }
}

static void test_position_selection_and_ui_guard(void)
{
    const struct w3cs_quality_profile *profile = w3cs_quality_profile(5);
    struct w3cs_quality_projection projection = orthographic_projection();
    float vertices[2][3] = {{100.0f, 0.0f, 0.0f}, {120.0f, 1.0f, 0.0f}};
    struct w3cs_quality_position_batch batch = {
        vertices, 2, sizeof(vertices[0]), 0, W3CS_QUALITY_WORLD_XYZ,
        false, &projection
    };
    float step;
    float error;
    int bits = w3cs_quality_choose_position_drop_bits(profile, &batch,
                                                       &step, &error);
    /* x and y map at 6.4 and 3.6 px/world-unit. The conservative Euclidean
     * corner bound rejects a 0.25-unit step and accepts 0.125. */
    assert(bits == 3);
    assert(step == 0.125f);
    assert(error <= profile->maximum_position_error_px + 1e-5f);
    batch.geometry_class = W3CS_QUALITY_SCREEN_XYZRHW;
    assert(w3cs_quality_choose_position_drop_bits(profile, &batch,
                                                   &step, &error) == -1);
    batch.geometry_class = W3CS_QUALITY_WORLD_XYZ;
    batch.protected_content = true;
    assert(w3cs_quality_choose_position_drop_bits(profile, &batch,
                                                   &step, &error) == -1);
    batch.protected_content = false;
    vertices[0][0] = 0.1f;
    vertices[1][0] = 0.2f;
    assert(w3cs_quality_choose_position_drop_bits(profile, &batch,
                                                   &step, &error) == -1);
}

static void test_position_selection_rejects_source_outside_depth(void)
{
    const struct w3cs_quality_profile *profile = w3cs_quality_profile(5);
    struct w3cs_quality_projection projection = unit_depth_projection();
    float vertices[2][3] = {
        {100.0f, 0.0f, 0.5f},
        {120.0f, 1.0f, 0.5f}
    };
    struct w3cs_quality_position_batch batch = {
        vertices, 2, sizeof(vertices[0]), 0, W3CS_QUALITY_WORLD_XYZ,
        false, &projection
    };
    float step;
    float error;

    assert(w3cs_quality_choose_position_drop_bits(profile, &batch,
                                                   &step, &error) == 5);
    vertices[1][2] = -0.01f;
    assert(!w3cs_quality_measure_position_error_px(&batch, 0u, &error));
    assert(w3cs_quality_choose_position_drop_bits(profile, &batch,
                                                   &step, &error) == -1);
    assert(step == 0.0f);
    assert(isinf(error));

    vertices[1][2] = 1.01f;
    assert(!w3cs_quality_measure_position_error_px(&batch, 0u, &error));
    assert(w3cs_quality_choose_position_drop_bits(profile, &batch,
                                                   &step, &error) == -1);
}

static void test_position_selection_rejects_depth_crossing_cell(void)
{
    const struct w3cs_quality_profile *profile = w3cs_quality_profile(5);
    struct w3cs_quality_projection projection = unit_depth_projection();
    float vertices[1][3] = {{100.0f, 0.0f, 0.125f}};
    struct w3cs_quality_position_batch batch = {
        vertices, 1, sizeof(vertices[0]), 0, W3CS_QUALITY_WORLD_XYZ,
        false, &projection
    };
    float error;

    /* The source and the 1/64-unit cell are inside the near plane. The
     * profile-5 cell has a 0.25-unit half extent and crosses that plane. */
    assert(w3cs_quality_measure_position_error_px(&batch, 0u, &error));
    assert(!w3cs_quality_measure_position_error_px(&batch, 5u, &error));
    assert(w3cs_quality_choose_position_drop_bits(profile, &batch,
                                                   NULL, &error) == -1);
    assert(isinf(error));
}

static void test_depth_sensitive_position_floor(void)
{
    assert(w3cs_quality_clamp_depth_sensitive_position_drop(
        5, true, true, false, false) == 0);
    assert(w3cs_quality_clamp_depth_sensitive_position_drop(
        5, true, false, true, false) == 0);
    assert(w3cs_quality_clamp_depth_sensitive_position_drop(
        5, true, false, false, true) == 0);
    assert(w3cs_quality_clamp_depth_sensitive_position_drop(
        5, false, true, true, true) == 5);
    assert(w3cs_quality_clamp_depth_sensitive_position_drop(
        5, true, false, false, false) == 5);
    assert(w3cs_quality_clamp_depth_sensitive_position_drop(
        0, true, true, true, true) == 0);
    assert(w3cs_quality_clamp_depth_sensitive_position_drop(
        -1, true, true, true, true) == -1);
    assert(w3cs_quality_clamp_depth_sensitive_position_drop(
        W3CS_QUALITY_POSITION_PROTECTED, true, true, true, true)
        == W3CS_QUALITY_POSITION_PROTECTED);
}

static void test_projection_composition_and_precision_reuse(void)
{
    float world[16] = {0};
    float view[16] = {0};
    float projection[16] = {0};
    struct w3cs_quality_projection result;
    unsigned axis;
    for (axis = 0; axis < 4u; ++axis) {
        world[axis * 4u + axis] = 1.0f;
        view[axis * 4u + axis] = 1.0f;
        projection[axis * 4u + axis] = 1.0f;
    }
    world[12] = 10.0f;
    view[0] = 2.0f;
    projection[0] = 0.1f;
    assert(w3cs_quality_compose_projection(
        world, view, projection, 4.0f, 8.0f, 1280.0f, 720.0f, &result));
    assert(result.object_to_clip[0] == 0.2f);
    assert(result.object_to_clip[12] == 2.0f);
    assert(result.viewport_x == 4.0f && result.viewport_y == 8.0f);
    assert(!w3cs_quality_compose_projection(
        world, view, projection, 0.0f, 0.0f, 0.0f, 720.0f, &result));
    world[0] = INFINITY;
    assert(!w3cs_quality_compose_projection(
        world, view, projection, 0.0f, 0.0f, 1280.0f, 720.0f, &result));

    assert(w3cs_quality_position_precision_satisfies(-1, -1));
    assert(w3cs_quality_position_precision_satisfies(-1, 0));
    assert(w3cs_quality_position_precision_satisfies(-1, 9));
    assert(w3cs_quality_position_precision_satisfies(0, 0));
    assert(w3cs_quality_position_precision_satisfies(0, 9));
    assert(w3cs_quality_position_precision_satisfies(3, 5));
    assert(!w3cs_quality_position_precision_satisfies(3, 2));
    assert(!w3cs_quality_position_precision_satisfies(0, -1));
    assert(w3cs_quality_position_precision_satisfies(
        -1, W3CS_QUALITY_POSITION_PROTECTED));
    assert(w3cs_quality_position_precision_satisfies(
        W3CS_QUALITY_POSITION_PROTECTED,
        W3CS_QUALITY_POSITION_PROTECTED));
    assert(w3cs_quality_position_precision_satisfies(
        W3CS_QUALITY_POSITION_PROTECTED, 9));
    assert(!w3cs_quality_position_precision_satisfies(
        0, W3CS_QUALITY_POSITION_PROTECTED));
    assert(!w3cs_quality_position_precision_satisfies(16, 9));
}

static void test_level10_fixed_behavior_without_projection(void)
{
    float vertices[1][3] = {{100.0f, 0.0f, 0.0f}};
    struct w3cs_quality_position_batch batch = {
        vertices, 1, sizeof(vertices[0]), 0, W3CS_QUALITY_WORLD_XYZ,
        false, NULL
    };
    float step;
    assert(w3cs_quality_choose_position_drop_bits(
        w3cs_quality_profile(10), &batch, &step, NULL) == 0);
    assert(step == W3CS_QUALITY_CURRENT_POSITION_STEP);
}

static void test_normals(void)
{
    const struct w3cs_quality_profile *quality10 = w3cs_quality_profile(10);
    struct w3cs_quality_normal_result result;
    float previous[3] = {0.0f, 0.0f, 1.0f};
    float axis[3] = {0.0f, 0.0f, 1.0f};
    float small_rotation[3] = {0.003490651f, 0.0f, 0.999993908f};
    float coarse_current[3] = {0.6154344f, 0.6154344f, 0.49242356f};
    float coarse_previous[3] = {0.4803417f, 0.4803417f, 0.73385537f};
    unsigned level;
    assert(w3cs_quality_encode_normal(quality10, axis, NULL, false,
                                      &result));
    assert(result.action == W3CS_QUALITY_NORMAL_EMIT);
    assert(result.octa_u == 0 && result.octa_v == 0);
    assert(memcmp(result.reconstructed, axis, sizeof(axis)) == 0);
    assert(w3cs_quality_encode_normal(quality10, small_rotation, previous,
                                      true, &result));
    assert(result.action == W3CS_QUALITY_NORMAL_HOLD);
    assert(result.angular_error_degrees < quality10->normal_hold_degrees);
    assert(w3cs_quality_encode_normal(w3cs_quality_profile(1),
                                      coarse_current, coarse_previous, true,
                                      &result));
    assert(result.action == W3CS_QUALITY_NORMAL_EXACT);
    assert(result.angular_error_degrees > 12.0f);

    /* Every emitted sample respects its profile's angular gate. An exact
     * fallback is valid for a direction that the codebook cannot represent
     * inside that gate. */
    for (level = 1; level <= 10; ++level) {
        const struct w3cs_quality_profile *profile =
            w3cs_quality_profile(level);
        unsigned latitude;
        unsigned longitude;
        for (latitude = 1; latitude < 18u; ++latitude) {
            for (longitude = 0; longitude < 36u; ++longitude) {
                float theta = (float)latitude * 3.14159265358979323846f
                    / 18.0f;
                float phi = (float)longitude * 2.0f
                    * 3.14159265358979323846f / 36.0f;
                float normal[3] = {
                    sinf(theta) * cosf(phi),
                    sinf(theta) * sinf(phi),
                    cosf(theta)
                };
                assert(w3cs_quality_encode_normal(profile, normal, NULL,
                                                  false, &result));
                if (result.action == W3CS_QUALITY_NORMAL_EMIT
                        || result.action == W3CS_QUALITY_NORMAL_HOLD)
                    assert(result.angular_error_degrees
                           <= profile->maximum_normal_error_degrees + 0.001f);
            }
        }
    }
}

static void test_cadence_and_anchor_safety(void)
{
    struct w3cs_quality_cadence cadence;
    const struct w3cs_quality_profile *emergency_profile =
        w3cs_quality_profile(1);
    unsigned frame;
    unsigned kept = 0;
    unsigned forced = 0;
    /* Cadence reduction is an explicit emergency experiment. It is not part
     * of the 10-to-1 quality profiles. */
    w3cs_quality_cadence_init(&cadence, 40, 12);
    for (frame = 0; frame < 400u; ++frame) {
        uint32_t flags = frame % 32u == 0u
            ? W3CS_QUALITY_FRAME_GEOMETRY_ANCHOR : 0u;
        struct w3cs_quality_frame_decision decision =
            w3cs_quality_cadence_decide(&cadence,
                                        (uint64_t)frame * 25000000u,
                                        flags);
        if (flags) {
            assert(decision.keep);
            assert(decision.reason == W3CS_QUALITY_FRAME_FORCED_KEEP
                   || frame == 0u);
            ++forced;
        }
        if (decision.keep) {
            assert(decision.require_anchor_base);
            ++kept;
        }
    }
    assert(forced == 13u);
    assert(kept >= 110u && kept <= 140u);
    assert(w3cs_quality_should_make_anchor(emergency_profile,
                                           0u, false));
    assert(!w3cs_quality_should_make_anchor(emergency_profile,
                                            7u, true));
    assert(w3cs_quality_should_make_anchor(emergency_profile,
                                           8u, true));
    /* A clock regression starts a new deterministic cadence phase and keeps
     * the first frame. */
    assert(w3cs_quality_cadence_decide(&cadence, 1u, 0u).keep);

    w3cs_quality_cadence_init(&cadence, 40, 40);
    for (frame = 0; frame < 100u; ++frame) {
        struct w3cs_quality_frame_decision decision =
            w3cs_quality_cadence_decide(&cadence,
                                        (uint64_t)frame * 25000000u, 0u);
        assert(decision.keep);
        assert(!decision.require_anchor_base);
    }
}

static void test_bandwidth_model(void)
{
    struct w3cs_quality_bandwidth_estimate prior;
    unsigned level;
    assert(w3cs_quality_estimate_accepted_capture(1, &prior));
    assert(prior.projected_ratio > 0.5);
    assert(prior.projected_ratio < 1.0);
    for (level = 2; level <= 10; ++level) {
        struct w3cs_quality_bandwidth_estimate current;
        assert(w3cs_quality_estimate_accepted_capture(level, &current));
        assert(current.projected_ratio >= prior.projected_ratio);
        prior = current;
    }
    assert(prior.projected_total_bytes == 21392193u);
    assert(fabs(prior.projected_ratio - 1.0) < 1e-12);
}

int main(void)
{
    test_profile_ladder();
    test_half_delta_precision();
    test_uniform_texel_detection();
    test_position_quantizer();
    test_position_selection_and_ui_guard();
    test_position_selection_rejects_source_outside_depth();
    test_position_selection_rejects_depth_crossing_cell();
    test_depth_sensitive_position_floor();
    test_projection_composition_and_precision_reuse();
    test_level10_fixed_behavior_without_projection();
    test_normals();
    test_cadence_and_anchor_safety();
    test_bandwidth_model();
    puts("w3cs_quality_test: all tests passed");
    return 0;
}
