/**
 * Joystick display modes
 * - Idle: Joystick is not being used
 * - Active: Joystick is being touched but not moved
 * - Moving: Joystick is being dragged
 */
enum JoystickDisplayMode {
  Idle = 0, // Initial state, joystick is inactive
  Active, // Joystick is touched but not yet moved
  Moving, // Joystick is being dragged
}

/**
 * Joystick state structure
 * - displayMode: Current display mode of the joystick
 * - pullingDistance: Distance the joystick is pulled from center (in pixels)
 * - normalizedDirection: Normalized direction vector of joystick pull (x,y from -1 to 1)
 */
type JoystickState = {
  displayMode: JoystickDisplayMode; // Current display mode of the joystick
  pullingDistance: number; // Distance the joystick is pulled from center (in pixels)
  normalizedDirection: APJS.Vector2f; // Normalized direction vector of joystick pull (x,y from -1 to 1)
};

/**
 * AR Character Controller
 * This class handles character movement, joystick input, and animations
 * for an AR character in the scene.
 */
@component()
export class ARCharacterController extends APJS.BasicScriptComponent {
  @serializeProperty
  private walkingSpeed: number = 20; // Walking speed of the character

  @serializeProperty
  private runningSpeed: number = 30; // Running speed of the character

  // 2D Joystick Controller serialized properties
  @groupBegin("2D Joystick Controller")
  @serializeProperty
  private uiCameraObject: APJS.SceneObject; // UI camera for screen space calculations

  @serializeProperty
  private joystickBase: APJS.SceneObject; // Joystick base object

  @serializeProperty
  private joystickTop: APJS.SceneObject; // Joystick top object that gets dragged

  @serializeProperty
  private joystickIndicator: APJS.SceneObject; // Joystick direction indicator

  @serializeProperty
  private waveButton: APJS.SceneObject; // Wave animation button

  @serializeProperty
  private danceButton: APJS.SceneObject; // Dance animation button
  @groupEnd()

  // 3D Character serialized properties
  @groupBegin("3D Character")
  @serializeProperty
  private arCameraObject: APJS.SceneObject; // AR camera object
  @serializeProperty
  private characterObject: APJS.SceneObject; // 3D character object
  @serializeProperty
  private characterAnimatorObject: APJS.SceneObject; // Animator object for character animations
  @groupEnd()

  // 2D Joystick Controller
  private uiCamera: APJS.Camera; // UI camera reference for screen space calculations
  private joystickBaseTransform: APJS.ScreenTransform; // Joystick base transform component
  private joystickTopTransform: APJS.ScreenTransform; // Joystick top transform component
  private joystickIndicatorTransform: APJS.ScreenTransform; // Joystick indicator transform component
  private joystickBaseImage: APJS.Image; // Joystick base image component
  private joystickTopImage: APJS.Image; // Joystick top image component
  private joystickIndicatorImage: APJS.Image; // Joystick indicator image component
  private joystickTopRadius: number = 0; // Radius of joystick top in pixels
  private maxPullingDistance: number = 100; // Maximum distance joystick can be pulled from center
  private currentJoystickState: JoystickState = {
    // Current state of the joystick
    displayMode: JoystickDisplayMode.Idle,
    pullingDistance: 0,
    normalizedDirection: new APJS.Vector2f(0, 0),
  };

  // 2D Buttons
  private waveButtonTransform: APJS.ScreenTransform; // Wave button transform component
  private waveButtonImage: APJS.Image; // Wave button image component
  private waveButtonRadius: number = 0; // Radius of wave button in pixels
  private danceButtonTransform: APJS.ScreenTransform; // Dance button transform component
  private danceButtonImage: APJS.Image; // Dance button image component
  private danceButtonRadius: number = 0; // Radius of dance button in pixels

  // 3D Character
  private arCamera: APJS.Camera;
  private characterTransform: APJS.Transform; // Character transform component
  private characterStartPosition: APJS.Vector3f; // Character position at start (used to reset when outside camera)
  private characterStartRotation: APJS.Quaternionf; // Character rotation at start (used to reset when outside camera)
  /** XZ offset from AR camera to character when start was captured: characterPos - cameraPos on XZ plane. */
  private characterStartVector: APJS.Vector3f; // (dx, 0, dz) for reset relative to current camera
  private characterAnimator: APJS.Animator; // Character animator component
  private characterAnimations: APJS.Animation[] = []; // Character animations array
  private idleAnimDelayTime: number = 0; // Delay time before playing idle animation

  // Constants
  private readonly numOfAnimations: number = 5; // Total number of character animations
  private readonly walkToRunThreshold: number = 0.8; // Threshold for switching from walk to run animation
  /** Viewport edge margin (0–1). Touch within this distance from screen edge is treated as "outside" and resets joystick. */
  private readonly touchEdgeMargin: number = 0.03;

  /**
   * Check if a tap position is within the radius of a screen transform
   * @param viewportPosition - Tap position in viewport coordinates (0-1)
   * @param targetTransform - Target screen transform to check against
   * @param targetRadius - Radius of the target area in pixels
   * @returns True if tap is within the target radius, false otherwise
   */
  private checkTapScreenTransform(
    viewportPosition: APJS.Vector2f,
    targetTransform: APJS.ScreenTransform,
    targetRadius: number,
  ): boolean {
    if (!this.uiCamera) {
      return false;
    }

    // Get target position in world space
    const targetWorldPos = targetTransform.getWorldPosition();

    // Convert to screen space (pixels)
    const targetScreenPos = this.uiCamera.worldToScreenPoint(targetWorldPos);

    // Create a point in world space at (0,0,0) to get screen dimensions
    const originWorld = new APJS.Vector3f(0, 0, 0);
    const originScreen = this.uiCamera.worldToScreenPoint(originWorld);

    // Calculate screen dimensions
    const screenWidth = originScreen.x * 2; // Assuming camera is centered
    const screenHeight = originScreen.y * 2;

    // Convert viewport coordinates (0-1) to screen coordinates (pixels)
    const clickScreenX = viewportPosition.x * screenWidth;
    const clickScreenY = viewportPosition.y * screenHeight;

    // Calculate distance between click position and target position in screen space
    const distance = Math.sqrt(
      Math.pow(clickScreenX - targetScreenPos.x, 2) +
        Math.pow(clickScreenY - targetScreenPos.y, 2),
    );

    // Check if distance is within the target radius
    return distance <= targetRadius;
  }

  /**
   * Move the joystick top based on touch input. Clamps pull to maxPullingDistance, updates normalizedDirection and pullingDistance, and sets joystick top world position (preserving Z).
   * @param viewportPosition - Touch position in viewport coordinates (0-1), Y flipped from screen (1 - screenY)
   */
  private moveJoystickTop(viewportPosition: APJS.Vector2f) {
    if (
      !this.uiCamera ||
      !this.joystickBaseTransform ||
      !this.joystickTopTransform
    ) {
      return;
    }

    // Get joystick base position in world space
    const joystickBaseWorldPos = this.joystickBaseTransform.getWorldPosition();
    // Convert to screen space
    const joystickBaseScreenPos =
      this.uiCamera.worldToScreenPoint(joystickBaseWorldPos);

    // Get joystick top position in world space to preserve Z coordinate
    const joystickTopWorldPos = this.joystickTopTransform.getWorldPosition();
    // Convert to screen space to get current Z depth
    const joystickTopScreenPos =
      this.uiCamera.worldToScreenPoint(joystickTopWorldPos);

    // Create a point in world space at (0,0,0) to get screen dimensions
    const originWorld = new APJS.Vector3f(0, 0, 0);
    const originScreen = this.uiCamera.worldToScreenPoint(originWorld);

    // Calculate screen dimensions
    const screenWidth = originScreen.x * 2; // Assuming camera is centered
    const screenHeight = originScreen.y * 2;

    // Convert viewport coordinates (0-1) to screen coordinates (pixels)
    const touchScreenX = viewportPosition.x * screenWidth;
    const touchScreenY = viewportPosition.y * screenHeight;

    // Calculate direction from joystick base to touch position
    const directionX = touchScreenX - joystickBaseScreenPos.x;
    const directionY = touchScreenY - joystickBaseScreenPos.y;

    // Calculate distance
    const distance = Math.sqrt(
      directionX * directionX + directionY * directionY,
    );

    // Clamp distance to maxPullingDistance
    const clampedDistance = Math.min(distance, this.maxPullingDistance);

    // Calculate normalized direction
    let normalizedX = 0;
    let normalizedY = 0;
    if (distance > 0) {
      normalizedX = directionX / distance;
      normalizedY = directionY / distance;
    }

    // Update currentJoystickState
    this.currentJoystickState.normalizedDirection.set(normalizedX, normalizedY);
    this.currentJoystickState.pullingDistance = clampedDistance;

    // Calculate new joystick top position in screen space
    // Use the same Z depth as the original joystick top
    const newJoystickTopScreenX =
      joystickBaseScreenPos.x + normalizedX * clampedDistance;
    const newJoystickTopScreenY =
      joystickBaseScreenPos.y + normalizedY * clampedDistance;
    const newJoystickTopScreenZ = joystickTopScreenPos.z; // Preserve original depth

    // Create screen space vector with preserved Z depth
    const newJoystickTopScreenPos = new APJS.Vector3f(
      newJoystickTopScreenX,
      newJoystickTopScreenY,
      newJoystickTopScreenZ,
    );

    // Convert screen space to world space
    const newJoystickTopWorldPos = this.uiCamera.screenToWorldPoint(
      newJoystickTopScreenPos,
    );

    // Set the new position
    this.joystickTopTransform.setWorldPosition(newJoystickTopWorldPos);
  }

  /**
   * Reset the joystick state to idle: display mode Idle, pull distance 0, direction (0,0). Recenters the joystick top and optionally plays idle animation.
   * @param withAnimation - If true, play idle animation when resetting (default true). Set false when switching to wave/dance.
   */
  private resetJoystickState(withAnimation: boolean = true) {
    // Reset joystick state to idle
    this.currentJoystickState = {
      displayMode: JoystickDisplayMode.Idle,
      pullingDistance: 0,
      normalizedDirection: new APJS.Vector2f(0, 0),
    };

    // Reset joystick top to center position
    if (this.joystickBaseTransform && this.joystickTopTransform) {
      const joystickBaseWorldPos =
        this.joystickBaseTransform.getWorldPosition();
      const joystickTopWorldPos = this.joystickTopTransform.getWorldPosition();
      // Preserve the joystick top's Z coordinate
      const newJoystickTopWorldPos = new APJS.Vector3f(
        joystickBaseWorldPos.x,
        joystickBaseWorldPos.y,
        joystickTopWorldPos.z,
      );
      this.joystickTopTransform.setWorldPosition(newJoystickTopWorldPos);
    }

    // Play idle animation if requested
    withAnimation && this.playAnimation("idle");
  }

  /**
   * Update the joystick display based on its current state. Sets base/top/indicator opacity: Idle = dimmed, Active = full with no indicator, Moving = full with indicator opacity from pull distance.
   * Called from onUpdate when joystick state may have changed.
   */
  private updateJoystickDisplay() {
    if (
      !this.joystickIndicatorImage ||
      !this.joystickBaseImage ||
      !this.joystickTopImage
    ) {
      return;
    }

    switch (this.currentJoystickState.displayMode) {
      case JoystickDisplayMode.Active:
        // Joystick is touched but not moved
        this.joystickBaseImage.opacity = 1;
        this.joystickTopImage.opacity = 1;
        this.joystickIndicatorImage.opacity = 0;
        break;
      case JoystickDisplayMode.Moving:
        // Joystick is being dragged
        this.joystickBaseImage.opacity = 1;
        this.joystickTopImage.opacity = 1;
        // Indicator opacity based on pulling distance
        this.joystickIndicatorImage.opacity = Math.max(
          0.3,
          this.currentJoystickState.pullingDistance / this.maxPullingDistance,
        );
        break;
      case JoystickDisplayMode.Idle:
      default:
        // Joystick is not being used
        this.joystickBaseImage.opacity = 0.4;
        this.joystickTopImage.opacity = 0.4;
        this.joystickIndicatorImage.opacity = 0;
        break;
    }
  }

  /**
   * Convert a 2D joystick direction to world-space AR direction using the AR camera's rotation. Joystick (0, -1) is camera forward; used for movement and for reset position (convertDirectionToAR(0, -1)).
   * @param directionX - Normalized joystick X direction (-1 to 1)
   * @param directionY - Normalized joystick Y direction (-1 to 1); use -1 for forward in camera space
   * @returns Converted direction vector in world XZ (Y = 0), normalized
   */
  private convertDirectionToAR(
    directionX: number,
    directionY: number,
  ): APJS.Vector3f {
    if (!this.arCamera) {
      return new APJS.Vector3f(directionX, 0, directionY);
    }

    // Get AR camera transform
    const arCameraTransform = this.arCameraObject.getTransform();
    if (!arCameraTransform) {
      return new APJS.Vector3f(directionX, 0, directionY);
    }

    // Get AR camera rotation
    const arCameraRotation = arCameraTransform.getWorldRotation();

    // Create a forward vector in joystick space (Y-axis is forward)
    const joystickForward = new APJS.Vector3f(directionX, 0, directionY);

    // Rotate the joystick forward vector by the AR camera's rotation
    // This converts it to the AR coordinate system
    const arForward = arCameraRotation.multiplyVector(joystickForward);

    // Normalize the resulting vector
    arForward.normalize();

    return arForward;
  }

  /**
   * Handle touch events for joystick, wave button, and dance button.
   * On Began: detects tap on joystick top or buttons; on Moved: drags joystick or resets if touch is near screen edge; on Ended: resets joystick and button colors.
   * @param event - Touch event object; event.args[0] is APJS.TouchData with phase and position
   */
  private onTouchEvent = (event: APJS.IEvent) => {
    const touchInfo = event.args[0] as APJS.TouchData;
    switch (touchInfo.phase) {
      case APJS.TouchPhase.Began:
        // Check if joystick top is touched
        const joystickTouched = this.checkTapScreenTransform(
          new APJS.Vector2f(touchInfo.position.x, 1 - touchInfo.position.y),
          this.joystickTopTransform,
          this.joystickTopRadius,
        );
        this.currentJoystickState.displayMode = joystickTouched
          ? JoystickDisplayMode.Active
          : JoystickDisplayMode.Idle;

        // Check if wave button is touched
        const waveButtonTouched = this.checkTapScreenTransform(
          new APJS.Vector2f(touchInfo.position.x, 1 - touchInfo.position.y),
          this.waveButtonTransform,
          this.waveButtonRadius,
        );
        if (waveButtonTouched) {
          this.resetJoystickState(false);
          this.playAnimation("wave");
          this.waveButtonImage.color = new APJS.Color(0.9, 0.9, 0.9, 1); // Highlight button
        }

        // Check if dance button is touched
        const danceButtonTouched = this.checkTapScreenTransform(
          new APJS.Vector2f(touchInfo.position.x, 1 - touchInfo.position.y),
          this.danceButtonTransform,
          this.danceButtonRadius,
        );
        if (danceButtonTouched) {
          this.resetJoystickState(false);
          this.playAnimation("dance");
          this.danceButtonImage.color = new APJS.Color(0.9, 0.9, 0.9, 1); // Highlight button
        }

        break;
      case APJS.TouchPhase.Moved:
        // Move joystick top if it's active
        if (
          this.currentJoystickState.displayMode !== JoystickDisplayMode.Idle
        ) {
          const { x, y } = touchInfo.position;
          const margin = this.touchEdgeMargin;
          const isNearEdge =
            x < margin || x > 1 - margin || y < margin || y > 1 - margin;
          if (isNearEdge) {
            this.resetJoystickState();
          } else {
            this.moveJoystickTop(new APJS.Vector2f(x, 1 - y));
          }
        }
        break;
      case APJS.TouchPhase.Ended:
        // Reset joystick if it was active
        if (
          this.currentJoystickState.displayMode !== JoystickDisplayMode.Idle
        ) {
          this.resetJoystickState();
        }
        // Reset button colors
        this.waveButtonImage.color = new APJS.Color(1, 1, 1, 1);
        this.danceButtonImage.color = new APJS.Color(1, 1, 1, 1);
        break;
    }
  };

  /**
   * Play a character animation by name. Idle, walk, and run loop; wave and dance play once then return to idle after their duration.
   * @param animationName - One of "idle" | "walk" | "run" | "wave" | "dance"
   */
  private playAnimation(
    animationName: "idle" | "walk" | "run" | "wave" | "dance",
  ) {
    if (
      this.characterAnimator &&
      this.characterAnimations.length >= this.numOfAnimations
    ) {
      // Map animation name to animation index
      let animationIndex = 0;
      switch (animationName) {
        case "idle":
          animationIndex = 0;
          break;
        case "walk":
          animationIndex = 1;
          break;
        case "run":
          animationIndex = 2;
          break;
        case "wave":
          animationIndex = 3;
          break;
        case "dance":
          animationIndex = 4;
          break;
      }

      if (this.characterAnimator.playback) {
        // Only play animation if it's different from current
        if (
          this.characterAnimator.playback.name !==
          this.characterAnimations[animationIndex].name
        )
          if (animationName === "wave" || animationName === "dance") {
            // Play wave or dance animation once
            this.characterAnimator.play(
              this.characterAnimations[animationIndex].name,
              APJS.AnimationWrapMode.Once,
              1,
            );
            // Set delay for idle animation based on animation duration
            this.idleAnimDelayTime =
              this.characterAnimations[animationIndex].duration;
          } else {
            // Play other animations in repeat mode
            this.characterAnimator.play(
              this.characterAnimations[animationIndex].name,
              APJS.AnimationWrapMode.Repeat,
              1,
            );
          }
      }
    }
  }

  /**
   * Handle record start event. Resets joystick state. If the character is outside the AR camera view, repositions it in front of the current camera using characterStartVector (distance + convertDirectionToAR) and makes it face the camera.
   * Called when recording starts.
   */
  private onRecordStart = () => {
    this.resetJoystickState();
    if (
      !this.characterTransform ||
      !this.arCamera ||
      !this.characterStartPosition ||
      !this.characterStartRotation
    ) {
      return;
    }
    const worldPos = this.characterTransform.getWorldPosition();
    const viewportPos = this.arCamera.worldToViewportPoint(worldPos);
    const inView =
      viewportPos.x >= 0 &&
      viewportPos.x <= 1 &&
      viewportPos.y >= 0 &&
      viewportPos.y <= 1;
    if (!inView) {
      // --- Reset position (camera-relative when possible) ---
      if (this.characterStartVector) {
        const arCamTransform = this.arCameraObject.getTransform();
        if (arCamTransform) {
          const cameraPos = arCamTransform.getWorldPosition();
          // Use stored XZ distance so character stays same distance from camera as at capture
          const distance = Math.sqrt(
            this.characterStartVector.x * this.characterStartVector.x +
              this.characterStartVector.z * this.characterStartVector.z,
          );
          // Current camera forward in world XZ (0, -1) = forward in joystick/camera space
          const arForward = this.convertDirectionToAR(0, -1);
          const newPosition = new APJS.Vector3f(
            cameraPos.x + arForward.x * distance,
            this.characterStartPosition.y,
            cameraPos.z + arForward.z * distance,
          );
          this.characterTransform.setWorldPosition(newPosition);
        } else {
          this.characterTransform.setWorldPosition(this.characterStartPosition);
        }
      } else {
        this.characterTransform.setWorldPosition(this.characterStartPosition);
      }
      // --- Reset rotation: face the AR camera (Y-only, same logic as movement) ---
      const arCamTransform = this.arCameraObject.getTransform();
      if (arCamTransform) {
        const cameraPos = arCamTransform.getWorldPosition();
        const charPos = this.characterTransform.getWorldPosition();
        const dx = cameraPos.x - charPos.x;
        const dz = cameraPos.z - charPos.z;
        const len = Math.sqrt(dx * dx + dz * dz);
        if (len > 1e-6) {
          const angle = Math.atan2(dx, dz) * (180 / Math.PI);
          const eulerAngles = new APJS.Vector3f(0, angle * (Math.PI / 180), 0);
          this.characterTransform.setWorldRotation(
            APJS.Quaternionf.makeFromEulerAngles(eulerAngles),
          );
        } else {
          this.characterTransform.setWorldRotation(this.characterStartRotation);
        }
      } else {
        this.characterTransform.setWorldRotation(this.characterStartRotation);
      }
    }
  };

  /**
   * Handle record end event. Resets joystick state when recording ends.
   * Called when recording ends.
   */
  private onRecordEnd = () => {
    this.resetJoystickState();
  };

  /**
   * Initialize the component. Resolves UI camera, joystick/button transforms and images, character transform/animator, computes joystick and button radii, and registers Touch / RecordStart / RecordEnd listeners.
   * Called when the component is first created.
   */
  onStart() {
    // Get UI camera reference
    this.uiCamera = this.uiCameraObject.getComponent("Camera") as APJS.Camera;

    // Get joystick component references
    this.joystickBaseTransform = this.joystickBase.getComponent(
      "ScreenTransform",
    ) as APJS.ScreenTransform;
    this.joystickTopTransform = this.joystickTop.getComponent(
      "ScreenTransform",
    ) as APJS.ScreenTransform;
    this.joystickIndicatorTransform = this.joystickIndicator.getComponent(
      "ScreenTransform",
    ) as APJS.ScreenTransform;
    this.joystickBaseImage = this.joystickBase.getComponent(
      "Image",
    ) as APJS.Image;
    this.joystickTopImage = this.joystickTop.getComponent(
      "Image",
    ) as APJS.Image;
    this.joystickIndicatorImage = this.joystickIndicator.getComponent(
      "Image",
    ) as APJS.Image;

    // Get button component references
    this.waveButtonTransform = this.waveButton.getComponent(
      "ScreenTransform",
    ) as APJS.ScreenTransform;
    this.danceButtonTransform = this.danceButton.getComponent(
      "ScreenTransform",
    ) as APJS.ScreenTransform;
    this.waveButtonImage = this.waveButton.getComponent("Image") as APJS.Image;
    this.danceButtonImage = this.danceButton.getComponent(
      "Image",
    ) as APJS.Image;

    // Get character component references
    this.arCamera = this.arCameraObject.getComponent("Camera") as APJS.Camera;
    this.characterTransform = this.characterObject.getTransform();
    this.characterAnimator = this.characterAnimatorObject.getComponent(
      "Animator",
    ) as APJS.Animator;
    if (this.characterAnimator) {
      this.characterAnimations = this.characterAnimator.animations;
    }

    // Initialize joystick state
    this.resetJoystickState();

    // Calculate joystick dimensions
    const joystickTopSize = this.joystickTopTransform.sizeDelta;
    this.joystickTopRadius = Math.min(joystickTopSize.x, joystickTopSize.y) / 2;
    const joystickBaseSize = this.joystickBaseTransform.sizeDelta;
    const baseRadiusPixels =
      Math.min(joystickBaseSize.x, joystickBaseSize.y) / 2;
    this.maxPullingDistance = baseRadiusPixels - this.joystickTopRadius;

    // Calculate button dimensions
    const waveButtonSize = this.waveButtonTransform.sizeDelta;
    this.waveButtonRadius = Math.min(waveButtonSize.x, waveButtonSize.y) / 2;
    const danceButtonSize = this.danceButtonTransform.sizeDelta;
    this.danceButtonRadius = Math.min(danceButtonSize.x, danceButtonSize.y) / 2;

    // Register event listeners
    APJS.EventManager.getGlobalEmitter().on(
      APJS.EventType.Touch,
      this.onTouchEvent,
    );
    APJS.EventManager.getGlobalEmitter().on(
      APJS.EventType.RecordStart,
      this.onRecordStart,
    );
    APJS.EventManager.getGlobalEmitter().on(
      APJS.EventType.RecordEnd,
      this.onRecordEnd,
    );
  }

  /**
   * Update the component each frame: applies joystick movement and rotation to the character (using convertDirectionToAR), captures characterStartPosition/characterStartVector/characterStartRotation on first move, updates joystick indicator rotation and display, and advances idle animation delay.
   * @param deltaTime - Time since last frame in seconds
   */
  onUpdate(deltaTime: number) {
    if (!this.characterTransform || !this.arCamera) {
      return;
    }

    // Get normalized direction from joystick
    // Note: directionY is inverted to match expected coordinate system
    const directionX = this.currentJoystickState.normalizedDirection.x;
    const directionY = -1 * this.currentJoystickState.normalizedDirection.y;

    // Calculate movement speed based on pulling distance
    const pullingRatio =
      this.currentJoystickState.pullingDistance / this.maxPullingDistance;
    const moveSpeed =
      pullingRatio < this.walkToRunThreshold
        ? this.walkingSpeed
        : this.runningSpeed;

    // Check if there's any significant input (threshold of 0.1)
    if (Math.abs(directionX) > 0.1 || Math.abs(directionY) > 0.1) {
      this.currentJoystickState.displayMode = JoystickDisplayMode.Moving;
    }

    // Handle character movement and rotation
    if (this.currentJoystickState.displayMode === JoystickDisplayMode.Moving) {
      // Initialize character start position, rotation, and XZ vector from camera if not already set
      if (!this.characterStartPosition || !this.characterStartVector) {
        const startPos = this.characterTransform.getWorldPosition();
        this.characterStartPosition = new APJS.Vector3f(
          startPos.x,
          startPos.y,
          startPos.z,
        );
        const arCamTransform = this.arCameraObject.getTransform();
        if (arCamTransform) {
          const camPos = arCamTransform.getWorldPosition();
          this.characterStartVector = new APJS.Vector3f(
            startPos.x - camPos.x,
            0,
            startPos.z - camPos.z,
          );
        }
      }
      if (!this.characterStartRotation) {
        const startRot = this.characterTransform.getWorldRotation();
        this.characterStartRotation = new APJS.Quaternionf(
          startRot.x,
          startRot.y,
          startRot.z,
          startRot.w,
        );
      }
      // Convert joystick input to AR coordinate system
      const arDirection = this.convertDirectionToAR(directionX, directionY);

      // Calculate rotation angle (y-axis rotation) to face movement direction
      const angle = Math.atan2(arDirection.x, arDirection.z) * (180 / Math.PI);

      // Create a quaternion for the rotation
      const eulerAngles = new APJS.Vector3f(0, angle * (Math.PI / 180), 0); // Convert degrees to radians
      const rotation = APJS.Quaternionf.makeFromEulerAngles(eulerAngles);

      // Set the character's rotation
      this.characterTransform.setWorldRotation(rotation);

      // Calculate movement vector using the AR direction
      const movement = new APJS.Vector3f(
        arDirection.x * moveSpeed * deltaTime,
        0, // No vertical movement
        arDirection.z * moveSpeed * deltaTime,
      );

      // Get current character position
      const currentPosition = this.characterTransform.getWorldPosition();

      // Calculate new position
      const newPosition = new APJS.Vector3f(
        currentPosition.x + movement.x,
        currentPosition.y + movement.y,
        currentPosition.z + movement.z,
      );

      // Set new position
      this.characterTransform.setWorldPosition(newPosition);

      // Play walk or run animation based on pulling distance
      this.playAnimation(
        this.currentJoystickState.pullingDistance / this.maxPullingDistance >
          this.walkToRunThreshold
          ? "run"
          : "walk",
      );
    }

    // Rotate joystick indicator to match stick direction (2D angle in degrees)
    if (this.joystickIndicatorTransform) {
      const nd = this.currentJoystickState.normalizedDirection;
      const angleDeg = Math.atan2(nd.x, -nd.y) * (180 / Math.PI) - 90;
      this.joystickIndicatorTransform.rotation = angleDeg;
      this.updateJoystickDisplay();
    }

    // Handle idle animation delay
    if (this.idleAnimDelayTime > 0) {
      this.idleAnimDelayTime -= deltaTime;
      if (this.idleAnimDelayTime <= 0) {
        this.idleAnimDelayTime = 0;
        this.playAnimation("idle");
      }
    }
  }

  /**
   * Clean up the component. Unregisters Touch, RecordStart, and RecordEnd event listeners.
   * Called when the component is destroyed.
   */
  onDestroy() {
    // Unregister event listeners to prevent memory leaks
    APJS.EventManager.getGlobalEmitter().off(
      APJS.EventType.Touch,
      this.onTouchEvent,
    );
    APJS.EventManager.getGlobalEmitter().off(
      APJS.EventType.RecordStart,
      this.onRecordStart,
    );
    APJS.EventManager.getGlobalEmitter().off(
      APJS.EventType.RecordEnd,
      this.onRecordEnd,
    );
  }
}
