import { assert } from "../../../assert.js";
import { clamp } from "../../../math/clamp.js";
import { sign_not_zero } from "../../../math/sign_not_zero.js";
import { computeHashFloat } from "../../../primitives/numbers/computeHashFloat.js";
import { randomPointInCircle } from "../../random/randomPointInCircle.js";
import { randomPointInSphere } from "../../random/randomPointInSphere.js";
import { v3_length } from "../../vec3/v3_length.js";
import { AbstractShape3D } from "./AbstractShape3D.js";
import { compute_signed_distance_gradient_by_sampling } from "./util/compute_signed_distance_gradient_by_sampling.js";

const scratch_v3 = [0, 0, 0];

/**
 * A capsule aligned with the Y axis. Composed of a cylindrical section of
 * length {@link height} and two hemispherical caps of radius {@link radius}.
 * Total extent along Y is `height + 2 * radius`.
 */
export class CapsuleShape3D extends AbstractShape3D {
    constructor() {
        super();

        /**
         * Radius of the hemispherical caps and the cylindrical section
         * @type {number}
         */
        this.radius = 0.5;

        /**
         * Length of the cylindrical (straight) section, NOT including the hemispheres
         * @type {number}
         */
        this.height = 1;
    }

    /**
     * @param {number} radius
     * @param {number} height
     * @returns {CapsuleShape3D}
     */
    static from(radius, height) {
        assert.isNumber(radius, 'radius');
        assert.isNumber(height, 'height');
        assert.greaterThanOrEqual(radius, 0, 'radius');
        assert.greaterThanOrEqual(height, 0, 'height');

        const r = new CapsuleShape3D();
        r.radius = radius;
        r.height = height;

        return r;
    }

    get volume() {
        const r = this.radius;
        const h = this.height;

        // cylindrical core + full sphere (two hemispheres)
        return Math.PI * r * r * h + (4 / 3) * Math.PI * r * r * r;
    }

    get surface_area() {
        const r = this.radius;
        const h = this.height;

        // side of the cylinder + full sphere surface (two hemispheres)
        return 2 * Math.PI * r * h + 4 * Math.PI * r * r;
    }

    compute_bounding_box(result) {
        const r = this.radius;
        const half_h = this.height / 2;

        result[0] = -r;
        result[1] = -half_h - r;
        result[2] = -r;
        result[3] = r;
        result[4] = half_h + r;
        result[5] = r;
    }

    signed_distance_at_point(point) {
        const px = point[0];
        const py = point[1];
        const pz = point[2];

        const half_h = this.height / 2;

        // collapse Y onto the central segment
        const y_seg = clamp(py, -half_h, half_h);

        const dy = py - y_seg;

        return v3_length(px, dy, pz) - this.radius;
    }

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

    contains_point(point) {
        const px = point[0];
        const py = point[1];
        const pz = point[2];

        const half_h = this.height / 2;

        const y_seg = clamp(py, -half_h, half_h);

        const dy = py - y_seg;

        const r = this.radius;

        return px * px + dy * dy + pz * pz < r * r;
    }

    nearest_point_on_surface(result, reference) {
        const rx = reference[0];
        const ry = reference[1];
        const rz = reference[2];

        const half_h = this.height / 2;

        // closest point on the central segment
        const y_seg = clamp(ry, -half_h, half_h);

        const dx = rx;
        const dy = ry - y_seg;
        const dz = rz;

        const dist = v3_length(dx, dy, dz);

        const r = this.radius;

        if (dist === 0) {
            // reference is on the central axis, pick an arbitrary surface direction (+X)
            result[0] = r;
            result[1] = y_seg;
            result[2] = 0;
            return;
        }

        const scale = r / dist;

        result[0] = dx * scale;
        result[1] = y_seg + dy * scale;
        result[2] = dz * scale;
    }

    support(result, result_offset, direction_x, direction_y, direction_z) {
        const half_h = this.height / 2;
        const r = this.radius;

        // pick the hemisphere whose center is along +direction.y
        const cap_y = sign_not_zero(direction_y) * half_h;

        result[result_offset] = direction_x * r;
        result[result_offset + 1] = cap_y + direction_y * r;
        result[result_offset + 2] = direction_z * r;
    }

    sample_random_point_in_volume(result, result_offset, random) {
        const r = this.radius;
        const h = this.height;

        const cylinder_volume = Math.PI * r * r * h;
        const sphere_volume = (4 / 3) * Math.PI * r * r * r;
        const total_volume = cylinder_volume + sphere_volume;

        const pick = random() * total_volume;

        if (pick < cylinder_volume) {
            // sample inside the cylindrical core
            randomPointInCircle(random, scratch_v3, 0);

            const y = (random() - 0.5) * h;

            result[result_offset] = scratch_v3[0] * r;
            result[result_offset + 1] = y;
            result[result_offset + 2] = scratch_v3[1] * r;
        } else {
            // sample inside a hemispherical cap
            randomPointInSphere(random, scratch_v3, 0);

            const sy = scratch_v3[1];

            const cap_offset = sign_not_zero(sy) * (h / 2);

            result[result_offset] = scratch_v3[0] * r;
            result[result_offset + 1] = sy * r + cap_offset;
            result[result_offset + 2] = scratch_v3[2] * r;
        }
    }

    /**
     * @param {CapsuleShape3D} other
     * @returns {boolean}
     */
    equals(other) {
        return super.equals(other)
            && this.radius === other.radius
            && this.height === other.height;
    }

    hash() {
        const a = computeHashFloat(this.radius);
        const b = computeHashFloat(this.height);

        return ((a << 5) - a + b) | 0;
    }
}

/**
 * Fast type-check marker, mirroring `SphereShape3D.prototype.isSphereShape3D`
 * and `BoxShape3D.prototype.isBoxShape3D`. Lets the physics narrowphase
 * dispatch capsule-sphere / capsule-capsule / capsule-box pairs to
 * closed-form helpers (capsule = Minkowski sum of segment + sphere; all
 * three reduce to point-or-segment distance queries that GJK+EPA struggles
 * with on a smooth surface).
 * @readonly
 * @type {boolean}
 */
CapsuleShape3D.prototype.isCapsuleShape3D = true;
