import { assert } from "../../../core/assert.js";
import { allocate_m4 } from "../../../core/geom/3d/mat4/allocate_m4.js";
import { compose_matrix4_array } from "../../../core/geom/3d/mat4/compose_matrix4_array.js";
import { decompose_matrix_4_array } from "../../../core/geom/3d/mat4/decompose_matrix_4_array.js";
import { M4_IDENTITY } from "../../../core/geom/3d/mat4/M4_IDENTITY.js";
import { m4_multiply } from "../../../core/geom/3d/mat4/m4_multiply.js";
import Quaternion from "../../../core/geom/Quaternion.js";
import Vector3 from "../../../core/geom/Vector3.js";
import { TransformFlags } from "./TransformFlags.js";

/**
 *
 * @type {Float32Array}
 */
const scratch_matrix = new Float32Array(16);

/**
 * Default set of flags for any {@link Transform}
 * @type {TransformFlags|number}
 */
const FLAGS_DEFAULT = TransformFlags.AutomaticChangeDetection;

/**
 * A Transform represents the position, rotation, and scale of an object in 3D space.
 * Think of it like the object's location, orientation, and size.
 * It has properties for {@link position} (like coordinates), {@link rotation} (how it's oriented), and {@link scale} (how big it is).
 *
 * @example
 *  const t = new Transform();
 *  t.position.set(1,2,3);
 *
 * @author Alex Goldring
 * @copyright Company Named Limited (c) 2026
 */
export class Transform {

    /**
     * @type {Vector3}
     * @readonly
     */
    position = new Vector3(0, 0, 0);

    /**
     * Orientation represented as a quaternion.
     * If Euler (XYZ) angles are required - consult {@link Quaternion} for relevant method
     * @type {Quaternion}
     * @readonly
     */
    rotation = new Quaternion(0, 0, 0, 1);

    /**
     * @type {Vector3}
     * @readonly
     */
    scale = new Vector3(1, 1, 1);

    /**
     * Affine transform matrix, {@link position}, {@link rotation}, and {@link scale} must match, but shear can be different.
     * Note: this is managed by the {@link Transform} itself unless {@link TransformFlags.AutomaticChangeDetection} is disabled; do not modify the matrix directly.
     * Generally, if you want to modify it - use {@link fromMatrix} method instead.
     *
     * If you're not comfortable with matrices, just leave defaults on and stick to primary attributes i.e. {@link position}, {@link rotation}, and {@link scale}.
     * @readonly
     * @type {Float32Array}
     */
    matrix = allocate_m4();

    /**
     * Various bit flags, see {@link TransformFlags}
     * This should generally be accessed through {@link getFlag}/{@link setFlag} instead of modifying the value directly
     * @type {number}
     */
    flags = FLAGS_DEFAULT;

    constructor() {
        // Watch changes.
        // This creates a data dependency, which makes GC impossible if we keep references to position/rotation/scale
        // Please clone those properties if you need to keep them past the expiry of the Transform
        this.subscribe(this.#handle_component_change, this);
    }

    /**
     * Current "forward" direction
     * NOTE that this vector is not live, meaning that if you modify transform, previously-obtained result will no longer be valid
     * @returns {Vector3}
     */
    get forward() {
        const result = Vector3.forward.clone();

        result.applyDirectionMatrix4(this.matrix);

        return result;
    }

    /**
     * Current "up" direction
     * @return {Vector3}
     */
    get up() {
        const result = Vector3.up.clone();

        result.applyDirectionMatrix4(this.matrix);

        return result;
    }

    /**
     * Current "right" direction
     * @return {Vector3}
     */
    get right() {
        const result = Vector3.right.clone();

        result.applyDirectionMatrix4(this.matrix);

        return result;
    }

    /**
     * Attach change listener
     * @param {function} handler
     * @param {*} [thisArg]
     */
    subscribe(handler, thisArg) {
        this.position.onChanged.add(handler, thisArg);
        this.rotation.onChanged.add(handler, thisArg);
        this.scale.onChanged.add(handler, thisArg);
    }

    /**
     * Disconnect change listener
     * @param {function} handler
     * @param {*} [thisArg]
     */
    unsubscribe(handler, thisArg) {
        this.position.onChanged.remove(handler, thisArg);
        this.rotation.onChanged.remove(handler, thisArg);
        this.scale.onChanged.remove(handler, thisArg);
    }

    /**
     *
     * @private
     */
    #handle_component_change() {
        if (this.getFlag(TransformFlags.AutomaticChangeDetection)) {
            this.updateMatrix();
        }
    }

    /**
     *
     * @param {number|TransformFlags} flag
     * @returns {void}
     */
    setFlag(flag) {
        this.flags |= flag;
    }

    /**
     *
     * @param {number|TransformFlags} flag
     * @returns {void}
     */
    clearFlag(flag) {
        this.flags &= ~flag;
    }

    /**
     *
     * @param {number|TransformFlags} flag
     * @param {boolean} value
     */
    writeFlag(flag, value) {
        if (value) {
            this.setFlag(flag);
        } else {
            this.clearFlag(flag);
        }
    }

    /**
     *
     * @param {number|TransformFlags} flag
     * @returns {boolean}
     */
    getFlag(flag) {
        return (this.flags & flag) === flag;
    }

    /**
     * Update {@link matrix} attribute from current {@link position}/{@link rotation}/{@link scale}
     *
     * Useful for when {@link TransformFlags.AutomaticChangeDetection} is disabled.
     */
    updateMatrix() {
        compose_matrix4_array(this.matrix, this.position, this.rotation, this.scale);
    }

    /**
     *
     * @param {Vector3} target
     * @param {Vector3} [up]
     */
    lookAt(target, up = Vector3.up) {

        const position = this.position;

        const delta_x = target.x - position.x;
        const delta_y = target.y - position.y;
        const delta_z = target.z - position.z;

        if (delta_x === 0 && delta_y === 0 && delta_z === 0) {
            // Target is at the same location as this transform, no valid rotation, keep whatever we have.
            // Generally this is a programmer mistake, making any changes to the rotation would be undesirable.
            // The most valid option is to keep the current state.
            return;
        }

        this.rotation._lookRotation(
            delta_x, delta_y, delta_z,
            up.x, up.y, up.z
        );
    }

    fromJSON(json) {
        const jp = json.position;

        if (jp !== undefined) {
            this.position.fromJSON(jp);
        } else {
            this.position.copy(Vector3.zero);
        }

        const jr = json.rotation;

        if (jr !== undefined) {
            this.rotation.fromJSON(jr);
        } else {
            this.rotation.copy(Quaternion.identity);
        }

        const js = json.scale;

        if (js !== undefined) {
            this.scale.fromJSON(js);
        } else {
            this.scale.copy(Vector3.one);
        }
    }

    toJSON() {
        return {
            position: this.position.toJSON(),
            rotation: this.rotation.toJSON(),
            scale: this.scale.toJSON()
        };
    }

    /**
     * Set this transform to match `other`.
     * Afterward `this.equals(other) === true`.
     * The `other` is unchanged in the process.
     * @param {Transform} other
     */
    copy(other) {
        // prevent the matrix from being overridden
        this.clearFlag(TransformFlags.AutomaticChangeDetection);

        this.matrix.set(other.matrix);

        this.position.copy(other.position);
        this.rotation.copy(other.rotation);
        this.scale.copy(other.scale);

        assert.arrayEqual(this.matrix, other.matrix, 'matrices must be equal after copy');

        this.flags = other.flags;
    }

    /**
     *
     * @returns {Transform}
     */
    clone() {
        const clone = new Transform();

        clone.copy(this);

        return clone;
    }

    /**
     * Strict equality check
     * @param {Transform} other
     * @returns {boolean}
     */
    equals(other) {
        return this.position.equals(other.position)
            && this.rotation.equals(other.rotation)
            && this.scale.equals(other.scale);
    }

    /**
     *
     * @returns {number}
     */
    hash() {
        // only use position for hash, for speed, since in most applications position will vary the most
        return this.position.hash();
    }

    /**
     *
     * @param json
     * @returns {Transform}
     */
    static fromJSON(json) {
        const result = new Transform();

        result.fromJSON(json);

        return result;
    }

    /**
     *
     * @param {number[]|Float32Array} mat
     * @returns {Transform}
     */
    static fromMatrix(mat) {
        const result = new Transform();

        result.fromMatrix(mat);

        return result;
    }

    /**
     * This is a post-multiplication, `this = this * other`.
     * The result of the multiplication is written into this transform.
     *
     * @param {Transform} other
     * @returns {this}
     */
    multiply(other) {
        return this.multiplyTransforms(this, other);
    }

    /**
     * Multiply two transforms, result it written into this one
     * @param {Transform} a
     * @param {Transform} b
     * @returns {this}
     */
    multiplyTransforms(a, b) {
        assert.defined(a, 'a');
        assert.defined(b, 'b');
        assert.equal(a.isTransform, true, 'a.isTransform !== true');
        assert.equal(b.isTransform, true, 'b.isTransform !== true');

        m4_multiply(scratch_matrix, a.matrix, b.matrix);

        return this.fromMatrix(scratch_matrix);
    }

    /**
     *
     * @param {mat4|number[]|Float32Array} matrix 4x4 affine matrix
     * @returns {this}
     */
    fromMatrix(matrix) {
        assert.isArrayLike(matrix, 'matrix');
        assert.greaterThanOrEqual(matrix.length, 16, 'matrix.length must be >= 16');

        // we know we are changing the matrix, so we're going to need to disable the flag that sets matrix from position/rotation/scale changes
        const ad = this.getFlag(TransformFlags.AutomaticChangeDetection);

        this.clearFlag(TransformFlags.AutomaticChangeDetection);

        this.matrix.set(matrix);

        decompose_matrix_4_array(matrix, this.position, this.rotation, this.scale);

        // restore value of the flag
        this.writeFlag(TransformFlags.AutomaticChangeDetection, ad);

        return this;
    }

    /**
     * Write out the current transform to a supplied container
     * @param {number[]|Float32Array} [result] if not provided, `Float32Array` of size 16 will be created
     * @returns {number[]} same as the input
     */
    toMatrix(result = new Float32Array(16)) {

        compose_matrix4_array(result, this.position, this.rotation, this.scale);

        return result;
    }

    /**
     * reset transform, resulting transform is an identity matrix
     *      - position: [0,0,0]
     *      - rotation: [0,0,0,1]
     *      - scale:    [1,1,1]
     */
    makeIdentity() {
        this.fromMatrix(M4_IDENTITY);
    }

    /**
     * Identity Transform is where position is 0, rotation is 0 (un-rotated) and scale is 1
     * @returns {boolean}
     */
    isIdentity() {
        return this.position.equals(Vector3.zero)
            && this.rotation.equals(Quaternion.identity)
            && this.scale.equals(Vector3.one);
    }

    toString() {
        return `Transform{ position: ${this.position}, rotation: ${this.rotation}, scale: ${this.scale} }`;
    }

    /**
     * @deprecated use {@link Quaternion.rotateTowards} instead
     * @param {Quaternion} sourceQuaternion
     * @param {Vector3} targetVector
     * @param {Number} limit
     */
    static adjustRotation(
        sourceQuaternion,
        targetVector,
        limit = Infinity
    ) {
        console.warn('deprecated, use Transform.rotation.rotateTowards instead');

        const q = new Quaternion();

        q.lookRotation(targetVector);

        sourceQuaternion.rotateTowards(q, limit);
    }
}

/**
 * @readonly
 * @type {string}
 */
Transform.typeName = "Transform";

/**
 * Enables a fast type check, without having to import the class separately.
 * @readonly
 * @type {boolean}
 * @example
 *  if(t.isTransform){
 *      // t is a Transform, do stuff
 *  }
 */
Transform.prototype.isTransform = true;



