import { Matrix4, Object3D, Quaternion, Vector3 } from "three";

import { isDevEnvironment } from "../../engine/debug/index.js";
import { serializable } from "../../engine/engine_serialization.js";
import { lookAtObject } from "../../engine/engine_three_utils.js";
import { type UsdzBehaviour } from "../../engine-components/export/usdz/extensions/behavior/Behaviour.js";
import { ActionBuilder, BehaviorModel, TriggerBuilder, USDVec3 } from "../../engine-components/export/usdz/extensions/behavior/BehavioursBuilder.js";
import { USDObject } from "../../engine-components/export/usdz/ThreeUSDZExporter.js";
import { Behaviour } from "../Component.js";

/**
 * The [LookAt](https://engine.needle.tools/docs/api/LookAt) behaviour makes the object look at a target object or the camera.
 * It can also invert the forward direction and keep the up direction.
 * 
 * @summary Makes the object look at a target object or the camera
 * @category Everywhere Actions
 * @category Interactivity
 * @group Components
 */
export class LookAt extends Behaviour implements UsdzBehaviour {

    /**
     * The target object to look at. If not set, the main camera will be used.
     */
    @serializable(Object3D)
    target?: Object3D;

    /**
     * Inverts the forward direction.
     */
    @serializable()
    invertForward: boolean = false;

    /**
     * Keep the up direction.
     */
    @serializable()
    keepUpDirection: boolean = true;

    /**
     * Copy the target rotation.
     */
    @serializable()
    copyTargetRotation: boolean = false;

    private static flipYQuat: Quaternion = new Quaternion().setFromAxisAngle(new Vector3(0, 1, 0), Math.PI);

    /** @internal */
    onBeforeRender(): void {
        let target: Object3D | null | undefined = this.target;
        if (!target) {
            target = this.context.mainCamera;
            if (isDevEnvironment() && !this["__did_warn"]) {
                this["__did_warn"] = true;
                console.debug(`[LookAt] No target set on ${this.name}, using main camera as target.`);
            }
        }
        if (!target) return;

        let copyTargetRotation = this.copyTargetRotation;
        // Disable copy target rotation during VR/AR session: https://linear.app/needle/issue/NE-5634
        if (this.context.isInVR || this.context.isInPassThrough) {
            copyTargetRotation = false;
        }
        lookAtObject(this.gameObject, target, this.keepUpDirection, copyTargetRotation);

        if (this.invertForward)
            this.gameObject.quaternion.multiply(LookAt.flipYQuat);
    }

    /** @internal */
    createBehaviours(ext, model: USDObject, _context) {
        if (model.uuid === this.gameObject.uuid) {
            let alignmentTarget = model;

            // not entirely sure why we need to do this - looks like LookAt with up vector doesn't work properly in
            // QuickLook, so we need to introduce an empty parent and rotate the model by 90° around Y
            if (this.keepUpDirection) {
                const parent = USDObject.createEmptyParent(model);
                alignmentTarget = parent;

                // rotate by 90° - counter-rotation on the parent makes sure 
                // that without Preliminary Behaviours it still looks right
                const flip = this.invertForward ? -1 : 1;
                parent.setMatrix(parent.getMatrix().multiply(new Matrix4().makeRotationZ(Math.PI / 2 * flip)));
                model.setMatrix(model.getMatrix().multiply(new Matrix4().makeRotationZ(-Math.PI / 2 * flip)));
            }

            const lookAt = new BehaviorModel("lookat " + this.name,
                TriggerBuilder.sceneStartTrigger(),
                ActionBuilder.lookAtCameraAction(
                    alignmentTarget,
                    undefined,
                    this.invertForward ? USDVec3.back : USDVec3.forward,
                    this.keepUpDirection ? USDVec3.up : USDVec3.zero
                ),
            );
            ext.addBehavior(lookAt);
        }
    }
}