import Vector3 from "../../Vector3.js";
import { max2 } from "../../../math/max2.js";
import { max3 } from "../../../math/max3.js";
import { min2 } from "../../../math/min2.js";
import { sign_not_zero } from "../../../math/sign_not_zero.js";
import { computeHashFloat } from "../../../primitives/numbers/computeHashFloat.js";
import { randomPointInBox } from "../../random/randomPointInBox.js";
import { v3_length } from "../../vec3/v3_length.js";
import { aabb3_nearest_point_on_surface } from "../aabb/aabb3_nearest_point_on_surface.js";
import { AbstractShape3D } from "./AbstractShape3D.js";
import { compute_signed_distance_gradient_by_sampling } from "./util/compute_signed_distance_gradient_by_sampling.js";

/**
 * Axis-aligned box of arbitrary half-extents, expressed in the body's local
 * frame. The shape spans `[-half_extents.x, +half_extents.x]` along X and
 * symmetrically on Y / Z.
 *
 * This is the generalisation of {@link UnitCubeShape3D} (which is fixed to
 * `0.5` per axis). The physics narrowphase recognises both via the shared
 * `isBoxShape3D` prototype marker and reads `half_extents.x / y / z` to feed
 * the closed-form sphere-box and box-box paths.
 *
 * @author Alex Goldring
 * @copyright Company Named Limited (c) 2026
 */
export class BoxShape3D extends AbstractShape3D {
    constructor() {
        super();

        /**
         * Half-width / half-height / half-depth in the body's local frame.
         * @readonly
         * @type {Vector3}
         */
        this.half_extents = new Vector3(0.5, 0.5, 0.5);
    }

    /**
     * Convenience constructor.
     * @param {number} hx
     * @param {number} hy
     * @param {number} hz
     * @returns {BoxShape3D}
     */
    static from(hx, hy, hz) {
        const b = new BoxShape3D();
        b.half_extents.set(hx, hy, hz);
        return b;
    }

    /**
     * Convenience constructor for boxes specified by full side lengths
     * rather than half-extents.
     * @param {number} width
     * @param {number} height
     * @param {number} depth
     * @returns {BoxShape3D}
     */
    static from_size(width, height, depth) {
        return BoxShape3D.from(width * 0.5, height * 0.5, depth * 0.5);
    }

    get volume() {
        const h = this.half_extents;
        return 8 * h.x * h.y * h.z;
    }

    get surface_area() {
        const h = this.half_extents;
        return 8 * (h.x * h.y + h.y * h.z + h.z * h.x);
    }

    compute_bounding_box(result) {
        const h = this.half_extents;
        result[0] = -h.x; result[1] = -h.y; result[2] = -h.z;
        result[3] =  h.x; result[4] =  h.y; result[5] =  h.z;
    }

    nearest_point_on_surface(result, reference) {
        const h = this.half_extents;
        aabb3_nearest_point_on_surface(
            result,
            -h.x, -h.y, -h.z,
             h.x,  h.y,  h.z,
            reference[0], reference[1], reference[2]
        );
    }

    support(result, result_offset, direction_x, direction_y, direction_z) {
        const h = this.half_extents;
        result[result_offset]     = sign_not_zero(direction_x) * h.x;
        result[result_offset + 1] = sign_not_zero(direction_y) * h.y;
        result[result_offset + 2] = sign_not_zero(direction_z) * h.z;
    }

    signed_distance_gradient_at_point(result, point) {
        return compute_signed_distance_gradient_by_sampling(result, this, point);
    }

    signed_distance_at_point(point) {
        // SDF of an axis-aligned box centred at origin. Same closed form as
        // UnitCubeShape3D but parametrised by half_extents.
        const h = this.half_extents;
        const q_x = Math.abs(point[0]) - h.x;
        const q_y = Math.abs(point[1]) - h.y;
        const q_z = Math.abs(point[2]) - h.z;

        return v3_length(
            max2(q_x, 0),
            max2(q_y, 0),
            max2(q_z, 0)
        ) + min2(max3(q_x, q_y, q_z), 0);
    }

    contains_point(point) {
        const h = this.half_extents;
        const x = point[0];
        const y = point[1];
        const z = point[2];
        return x > -h.x && x < h.x
            && y > -h.y && y < h.y
            && z > -h.z && z < h.z;
    }

    sample_random_point_in_volume(result, result_offset, random) {
        randomPointInBox(random, result, result_offset);
        const h = this.half_extents;
        // randomPointInBox samples a unit cube — rescale to our half-extents.
        result[result_offset]     *= h.x * 2;
        result[result_offset + 1] *= h.y * 2;
        result[result_offset + 2] *= h.z * 2;
    }

    /**
     * @param {BoxShape3D} other
     * @returns {boolean}
     */
    equals(other) {
        return super.equals(other) && this.half_extents.equals(other.half_extents);
    }

    hash() {
        const h = this.half_extents;
        const a = computeHashFloat(h.x);
        const b = computeHashFloat(h.y);
        const c = computeHashFloat(h.z);
        return ((a * 31 + b) * 31 + c) | 0;
    }
}

/**
 * Fast type-check marker. Lets the physics narrowphase dispatch sphere-box
 * and box-box pairs against arbitrary-half-extent boxes without an instanceof
 * import or chain check. Read together with `shape.half_extents.x/y/z`.
 * @readonly
 * @type {boolean}
 */
BoxShape3D.prototype.isBoxShape3D = true;
