#include <math.h>

#include "cm_trace_core_private.h"

enum {
    CM_TEMP_CAPSULE_MODEL_HANDLE = 0x1fe
};

static const vec3_t cm_sight_trace_zero_vec = {0.0f, 0.0f, 0.0f};

int32_t
CM_SightTrace(int32_t result,
              const vec3_t start,
              const vec3_t end,
              const vec3_t mins,
              const vec3_t maxs,
              int32_t model,
              const vec3_t origin,
              int32_t brushMask,
              qboolean capsule,
              const cmTraceSphereRecord_t *sphere)
{
    const coduo_collision_model_t *collisionModel =
        CM_ClipHandleToModel(model);
    traceWork_t traceWork;
    vec3_t offset;

    (void)origin;

    cm_checkcount++;
    Com_Memset(&traceWork, 0, sizeof(traceWork));
    traceWork.trace.fraction = 1.0f;

    const float *traceMins = mins;
    if (traceMins == NULL) {
        traceMins = cm_sight_trace_zero_vec;
    }

    const float *traceMaxs = maxs;
    if (traceMaxs == NULL) {
        traceMaxs = cm_sight_trace_zero_vec;
    }

    traceWork.contents = brushMask;
    for (int32_t axis = 0; axis < 3; ++axis) {
        offset[axis] = (traceMins[axis] + traceMaxs[axis]) * 0.5f;
        traceWork.mins[axis] = traceMins[axis] - offset[axis];
        traceWork.maxs[axis] = traceMaxs[axis] - offset[axis];
        traceWork.start[axis] = start[axis] + offset[axis];
        traceWork.end[axis] = end[axis] + offset[axis];
        traceWork.delta[axis] =
            traceWork.end[axis] - traceWork.start[axis];
    }

    traceWork.deltaLengthSquared =
        (traceWork.delta[0] * traceWork.delta[0]) +
        (traceWork.delta[1] * traceWork.delta[1]) +
        (traceWork.delta[2] * traceWork.delta[2]);

    if (sphere != NULL) {
        traceWork.sphere = sphere->sphere;
        traceWork.sphereExtents[0] = sphere->extents[0];
        traceWork.sphereExtents[1] = sphere->extents[1];
        traceWork.sphereExtents[2] = sphere->extents[2];
    } else {
        traceWork.sphere.use = capsule;
        if (traceWork.maxs[2] < traceWork.maxs[0]) {
            traceWork.sphere.radius = traceWork.maxs[2];
        } else {
            traceWork.sphere.radius = traceWork.maxs[0];
        }
        traceWork.sphere.halfheight = traceWork.maxs[2];
        traceWork.sphere.offset[0] = 0.0f;
        traceWork.sphere.offset[1] = 0.0f;
        traceWork.sphere.offset[2] =
            traceWork.maxs[2] - traceWork.sphere.radius;
    }

    /* 0x805cf96..0x805cfa8: stock sums maxs into the (never-read) work
     * field at +0xa0 with one float rounding. */
    traceWork.maxsSum =
        traceWork.maxs[0] + traceWork.maxs[1] + traceWork.maxs[2];

    traceWork.offsets[0][0] = traceWork.mins[0];
    traceWork.offsets[0][1] = traceWork.mins[1];
    traceWork.offsets[0][2] = traceWork.mins[2];
    traceWork.offsets[1][0] = traceWork.maxs[0];
    traceWork.offsets[1][1] = traceWork.mins[1];
    traceWork.offsets[1][2] = traceWork.mins[2];
    traceWork.offsets[2][0] = traceWork.mins[0];
    traceWork.offsets[2][1] = traceWork.maxs[1];
    traceWork.offsets[2][2] = traceWork.mins[2];
    traceWork.offsets[3][0] = traceWork.maxs[0];
    traceWork.offsets[3][1] = traceWork.maxs[1];
    traceWork.offsets[3][2] = traceWork.mins[2];
    traceWork.offsets[4][0] = traceWork.mins[0];
    traceWork.offsets[4][1] = traceWork.mins[1];
    traceWork.offsets[4][2] = traceWork.maxs[2];
    traceWork.offsets[5][0] = traceWork.maxs[0];
    traceWork.offsets[5][1] = traceWork.mins[1];
    traceWork.offsets[5][2] = traceWork.maxs[2];
    traceWork.offsets[6][0] = traceWork.mins[0];
    traceWork.offsets[6][1] = traceWork.maxs[1];
    traceWork.offsets[6][2] = traceWork.maxs[2];
    traceWork.offsets[7][0] = traceWork.maxs[0];
    traceWork.offsets[7][1] = traceWork.maxs[1];
    traceWork.offsets[7][2] = traceWork.maxs[2];

    if (traceWork.sphere.use != 0) {
        for (int32_t axis = 0; axis < 3; ++axis) {
            /* 0x805d114: stock inlines the fabs here (no helper call). */
            const float sphereOffset =
                fabsf(traceWork.sphere.offset[axis]);

            if (traceWork.start[axis] < traceWork.end[axis]) {
                traceWork.bounds[0][axis] =
                    traceWork.start[axis] - sphereOffset -
                    traceWork.sphere.radius;
                traceWork.bounds[1][axis] =
                    sphereOffset + traceWork.end[axis] +
                    traceWork.sphere.radius;
            } else {
                traceWork.bounds[0][axis] =
                    traceWork.end[axis] - sphereOffset -
                    traceWork.sphere.radius;
                traceWork.bounds[1][axis] =
                    sphereOffset + traceWork.start[axis] +
                    traceWork.sphere.radius;
            }
        }
    } else {
        for (int32_t axis = 0; axis < 3; ++axis) {
            if (traceWork.start[axis] < traceWork.end[axis]) {
                traceWork.bounds[0][axis] =
                    traceWork.start[axis] + traceWork.mins[axis];
                traceWork.bounds[1][axis] =
                    traceWork.end[axis] + traceWork.maxs[axis];
            } else {
                traceWork.bounds[0][axis] =
                    traceWork.end[axis] + traceWork.mins[axis];
                traceWork.bounds[1][axis] =
                    traceWork.start[axis] + traceWork.maxs[axis];
            }
        }
    }

    traceWork.isPoint =
        (traceWork.maxs[0] + traceWork.maxs[1] + traceWork.maxs[2] ==
         0.0f);

    if (traceWork.sphere.use != 0) {
        for (int32_t axis = 0; axis < 3; ++axis) {
            /* 0x805d2c3: stock calls the CM_TraceFloatAbs helper here
             * (exact value either way; form matches the call). */
            traceWork.sphereExtents[axis] =
                traceWork.sphere.radius +
                CM_TraceFloatAbs(traceWork.sphere.offset[axis]);
        }
    }

    int32_t sightHit;
    if (model == 0) {
        sightHit = 0;
        if (result > 0) {
            const int32_t hitIndex = result - 1;

            if (hitIndex < cm_numBrushes) {
                sightHit =
                    CM_SightTraceThroughBrush(&traceWork,
                                              &cm_brushes[hitIndex]);
            } else if (hitIndex - cm_numBrushes < cm_numTerrainPatches) {
                /* ORIGINAL_BINARY_BUG: the terrain helper returns a boolean,
                 * but this cached path consumes it as a one-based hit id; see
                 * original-bugs/coduomp-sight-trace-cached-terrain-clear-hit.md. */
                sightHit = CM_SightTraceThroughPatch(
                    &traceWork,
                    &cm_terrainPatches[hitIndex - cm_numBrushes]);
            }
        }

        if (sightHit == 0) {
            sightHit = CM_SightTraceThroughTree(&traceWork, 0, 0.0f, 1.0f,
                                                traceWork.start,
                                                traceWork.end);
        }
    } else if (model == CM_TEMP_CAPSULE_MODEL_HANDLE) {
        if ((cm_boxBrush->contents & traceWork.contents) == 0) {
            sightHit = 0;
        } else if (traceWork.sphere.use == 0) {
            sightHit = CM_SightTraceBoundingBoxThroughCapsule(&traceWork);
        } else {
            sightHit = CM_SightTraceCapsuleThroughCapsule(&traceWork);
        }
    } else {
        sightHit = CM_SightTraceThroughLeaf(&traceWork,
                                            &collisionModel->leaf);
    }

    return sightHit;
}
