import { Transform } from "../../../../engine/ecs/transform/Transform.js";
import { assert } from "../../../assert.js";
import { isArrayEqualStrict } from "../../../collection/array/isArrayEqualStrict.js";
import { computeHashFloatArray } from "../../../math/hash/computeHashFloatArray.js";
import { sign } from "../../../math/sign.js";
import { v3_length } from "../../vec3/v3_length.js";
import { v3_matrix4_multiply } from "../../vec3/v3_matrix4_multiply.js";
import { aabb3_matrix4_project } from "../aabb/aabb3_matrix4_project.js";
import { m4_extract_scale } from "../mat4/m4_extract_scale.js";
import { m4_invert } from "../mat4/m4_invert.js";
import { AbstractShape3D } from "./AbstractShape3D.js";

/**
 *
 * @type {number[]|Float32Array|vec3}
 */
const scratch_v3_0 = new Float32Array(3);

export class TransformedShape3D extends AbstractShape3D {
    constructor() {
        super();

        /**
         *
         * @type {number[]|Float32Array|mat4}
         * @private
         */
        this.__matrix = new Float32Array(16);

        /**
         *
         * @type {Float32Array|mat4}
         * @private
         */
        this.__inverse_matrix = new Float32Array(16);

        /**
         *
         * @type {AbstractShape3D|null}
         * @private
         */
        this.__subject = null;
    }

    /**
     *
     * @returns {AbstractShape3D|null}
     */
    get subject() {
        return this.__subject;
    }

    /**
     * An affine transform preserves convexity, so a TransformedShape3D is
     * convex iff its subject is. A wrapper with no subject is treated as
     * non-convex (there's nothing to support against).
     * @returns {boolean}
     */
    get is_convex() {
        return this.__subject !== null && this.__subject.is_convex;
    }

    /**
     *
     * @param {number[]|Float32Array|mat4} m4
     * @param {AbstractShape3D} subject
     * @returns {TransformedShape3D}
     */
    static from_m4(subject, m4) {
        const r = new TransformedShape3D();

        r.__subject = subject;
        r.transform = m4;

        return r;
    }

    /**
     *
     * @param {AbstractShape3D} subject
     * @param {number[]} translation
     * @param {number[]} scale
     */
    static from_translation_scale(subject, translation, scale) {

        assert.isArrayLike(translation, 'translation');
        assert.greaterThanOrEqual(translation.length, 3, 'translation length must be >= 3');
        assert.isArrayLike(scale, 'scale');
        assert.greaterThanOrEqual(scale.length, 3, 'scale length must be >= 3');

        const t = new Transform();
        t.scale.fromArray(scale);
        t.position.fromArray(translation);

        return TransformedShape3D.from_m4(subject, t.matrix);
    }

    /**
     *
     * @param {AbstractShape3D} subject
     * @param {number[]} translation
     */
    static from_translation(subject, translation) {
        return TransformedShape3D.from_translation_scale(subject, translation, [1, 1, 1]);
    }

    /**
     *
     * @returns {TransformedShape3D}
     * @param {AbstractShape3D} subject
     * @param {number[]|Float32Array|vec3} scale
     */
    static from_scale(subject, scale) {

        const t = new Transform();
        t.scale.fromArray(scale);


        return TransformedShape3D.from_m4(subject, t.matrix);
    }

    /**
     *
     * @param {number[]|Float32Array} m
     */
    set transform(m) {
        this.__matrix.set(m);

        m4_invert(this.__inverse_matrix, this.__matrix);
    }

    get transform() {
        return this.__matrix;
    }

    get volume() {
        m4_extract_scale(scratch_v3_0, 0, this.__matrix);

        const scale_modifier = scratch_v3_0[0] * scratch_v3_0[1] * scratch_v3_0[2];

        return this.__subject.volume * scale_modifier;
    }

    compute_bounding_box(result) {
        const tmp = [];
        this.__subject.compute_bounding_box(tmp);

        aabb3_matrix4_project(result, tmp, this.__matrix);
    }

    signed_distance_gradient_at_point(result, point) {
        // transform point to subject's local space
        v3_matrix4_multiply(scratch_v3_0, 0, point, 0, this.__inverse_matrix);

        const subject = this.__subject;

        // sample gradient at the point
        const distance = subject.signed_distance_gradient_at_point(scratch_v3_0, scratch_v3_0);

        const m = this.__matrix;

        const m11 = m[0];
        const m12 = m[1];
        const m13 = m[2];

        const m21 = m[4];
        const m22 = m[5];
        const m23 = m[6];

        const m31 = m[8];
        const m32 = m[9];
        const m33 = m[10];

        const scale_x = v3_length(m11, m12, m13);
        const scale_y = v3_length(m21, m22, m23);
        const scale_z = v3_length(m31, m32, m33);

        const scaled_gx = scratch_v3_0[0] * scale_x;
        const scaled_gy = scratch_v3_0[1] * scale_y;
        const scaled_gz = scratch_v3_0[2] * scale_z;

        // normalize gradient
        const gd = distance / v3_length(scratch_v3_0[0], scratch_v3_0[1], scratch_v3_0[2]);

        const scaled_dx = scaled_gx * gd;
        const scaled_dy = scaled_gy * gd;
        const scaled_dz = scaled_gz * gd;

        // determine sign
        const result_sign = sign(distance);

        const scaled_distance = result_sign * Math.sqrt(scaled_dx * scaled_dx + scaled_dy * scaled_dy + scaled_dz * scaled_dz);

        // normalize the world-space gradient to unit length before storing
        const scaled_mag = v3_length(scaled_gx, scaled_gy, scaled_gz);

        if (scaled_mag > 0) {
            result[0] = scaled_gx / scaled_mag;
            result[1] = scaled_gy / scaled_mag;
            result[2] = scaled_gz / scaled_mag;
        } else {
            result[0] = 0;
            result[1] = 0;
            result[2] = 0;
        }

        return scaled_distance;
    }

    signed_distance_at_point(point) {
        return this.signed_distance_gradient_at_point(scratch_v3_0, point);
    }

    contains_point(point) {
        // transform point to subject's local space
        v3_matrix4_multiply(scratch_v3_0, 0, point, 0, this.__inverse_matrix);

        return this.__subject.contains_point(scratch_v3_0);
    }

    sample_random_point_in_volume(result, result_offset, random) {
        this.__subject.sample_random_point_in_volume(scratch_v3_0, 0, random);

        v3_matrix4_multiply(result, result_offset, scratch_v3_0, 0, this.__matrix);
    }

    support(result, result_offset, direction_x, direction_y, direction_z) {
        // Support function under a 4x4 affine transform.
        //
        // For a shape S = { M·x : x ∈ subject }, the support point is
        //   arg max_{p ∈ S} dot(p, d) = M · arg max_{x ∈ subject} dot(x, Mᵀ·d).
        // So we must transform the query direction by Mᵀ (the linear-part
        // transpose), normalise it (the subject's `support` contract
        // assumes a unit input), delegate to the subject, then push the
        // result through the full M.
        //
        // For a pure rigid rotation Mᵀ = M⁻¹ (orthonormal), which is the
        // "rotate the direction into body space first" intuition. For
        // uniform scale, Mᵀ·d differs from d only in magnitude and the
        // post-normalisation cancels that out. Non-uniform scale gets
        // the geometrically correct ellipsoid/box support direction
        // automatically (the support of a stretched shape in world
        // direction d is *not* the stretched version of the support in
        // direction d in body space — it's the result of stretching the
        // body-space support in direction Mᵀ·d, which is what this
        // computes).
        //
        // Without this, the previous body of `support` passed the world
        // direction straight to the subject — correct for pure
        // translation and identity rotation, wrong for everything else.
        const m = this.__matrix;
        // (Mᵀ · d)[i] = Σⱼ M[j, i] · d[j]. With column-major storage,
        // M[j, i] = m[j + i·4].
        let ld_x = m[0] * direction_x + m[1] * direction_y + m[2] * direction_z;
        let ld_y = m[4] * direction_x + m[5] * direction_y + m[6] * direction_z;
        let ld_z = m[8] * direction_x + m[9] * direction_y + m[10] * direction_z;

        const ld_len = Math.sqrt(ld_x * ld_x + ld_y * ld_y + ld_z * ld_z);
        if (ld_len > 0) {
            const inv = 1 / ld_len;
            ld_x *= inv; ld_y *= inv; ld_z *= inv;
        }

        this.__subject.support(scratch_v3_0, 0, ld_x, ld_y, ld_z);

        v3_matrix4_multiply(result, result_offset, scratch_v3_0, 0, this.__matrix);
    }

    /**
     *
     * @param {TransformedShape3D} other
     * @returns {boolean}
     */
    equals(other) {
        return super.equals(other)
            && this.__subject.equals(other.subject)
            && isArrayEqualStrict(this.__matrix, other.__matrix)
            ;
    }

    hash() {
        // to avoid recursion, only use matrix for hash
        return computeHashFloatArray(this.__matrix);
    }
}

/**
 * @readonly
 * @type {boolean}
 */
TransformedShape3D.isTransformedShape3D = true;
