/**
 * @typedef {import("../grid/types.js").Position} Position
 */

import * as THREE from "three";

/**
 * InputManager.js
 * Handles mouse interaction, raycasting, grid selection, keyboard, and Gamepad input.
 * Extends EventTarget for standard event handling.
 * @class
 */
export class InputManager extends EventTarget {
  /**
   * @param {THREE.Camera} camera - Three.js camera
   * @param {THREE.Scene} scene - Three.js scene
   * @param {HTMLElement} domElement - DOM element for input
   */
  constructor(camera, scene, domElement) {
    super();
    /** @type {THREE.Camera} */
    this.camera = camera;
    /** @type {THREE.Scene} */
    this.scene = scene;
    /** @type {HTMLElement} */
    this.domElement = domElement;

    /** @type {THREE.Raycaster} */
    this.raycaster = new THREE.Raycaster();
    /** @type {THREE.Vector2} */
    this.mouse = new THREE.Vector2();

    // Input State
    /** @type {Set<string>} */
    this.keys = new Set();
    /** @type {Map<number, { buttons: boolean[]; axes: number[] }>} */
    this.gamepads = new Map();
    /** @type {number} */
    this.axisDeadzone = 0.2; // Increased slightly for stability

    // Cursor State
    /** @type {THREE.Vector3} */
    this.cursorPos = new THREE.Vector3(0, 0, 0);
    /** @type {((x: number, y: number, z: number) => false | Position) | null} */
    this.cursorValidator = null; // Function to check if a position is valid

    // Visual Cursor (Wireframe Box)
    const geometry = new THREE.BoxGeometry(1.05, 1.05, 1.05);
    const material = new THREE.MeshBasicMaterial({
      color: 0xffff00,
      wireframe: true,
      transparent: true,
      opacity: 0.8,
    });
    this.cursor = new THREE.Mesh(geometry, material);
    this.cursor.visible = false;
    // Ensure cursor doesn't interfere with raycast
    this.cursor.raycast = () => {};
    this.scene.add(this.cursor);

    // Bindings
    this.onMouseMove = this.onMouseMove.bind(this);
    this.onMouseClick = this.onMouseClick.bind(this);
    this.onKeyDown = this.onKeyDown.bind(this);
    this.onKeyUp = this.onKeyUp.bind(this);
    this.onGamepadConnected = this.onGamepadConnected.bind(this);
    this.onGamepadDisconnected = this.onGamepadDisconnected.bind(this);

    this.attach();
  }

  attach() {
    this.domElement.addEventListener("mousemove", this.onMouseMove);
    this.domElement.addEventListener("click", this.onMouseClick);
    window.addEventListener("keydown", this.onKeyDown);
    window.addEventListener("keyup", this.onKeyUp);
    window.addEventListener("gamepadconnected", this.onGamepadConnected);
    window.addEventListener("gamepaddisconnected", this.onGamepadDisconnected);
  }

  detach() {
    this.domElement.removeEventListener("mousemove", this.onMouseMove);
    this.domElement.removeEventListener("click", this.onMouseClick);
    window.removeEventListener("keydown", this.onKeyDown);
    window.removeEventListener("keyup", this.onKeyUp);
    window.removeEventListener("gamepadconnected", this.onGamepadConnected);
    window.removeEventListener(
      "gamepaddisconnected",
      this.onGamepadDisconnected
    );

    if (this.cursor.parent) {
      this.cursor.parent.remove(this.cursor);
    }
  }

  // --- CURSOR MANIPULATION ---

  /**
   * Set a validation function to restrict cursor movement.
   * @param {(x: number, y: number, z: number) => false | Position} validatorFn - Takes (x, y, z). Returns false if invalid, or a modified position object.
   */
  setValidator(validatorFn) {
    this.cursorValidator = validatorFn;
  }

  /**
   * Programmatically move the cursor (e.g. via Gamepad or Keyboard).
   * This now simulates a raycast-like resolution to ensure consistent behavior.
   * @param {number} x - X coordinate
   * @param {number} y - Y coordinate
   * @param {number} z - Z coordinate
   */
  setCursor(x, y, z) {
    // Instead of raw rounding, we try to "snap" using the same logic as raycast
    // if possible, or just default to standard validation.

    // Since keyboard input gives us integer coordinates directly (usually),
    // we can treat them as the "Target Voxel".

    let targetX = Math.round(x);
    let targetY = Math.round(y);
    let targetZ = Math.round(z);

    // Validate or Adjust the target position if a validator is set
    if (this.cursorValidator) {
      const result = this.cursorValidator(targetX, targetY, targetZ);

      if (result === false) {
        return; // Invalid, abort
      } else if (typeof result === "object") {
        // Validator returned a corrected position (e.g. climbed a wall)
        targetX = result.x;
        targetY = result.y;
        targetZ = result.z;
      }
    }

    this.cursor.visible = true;
    this.cursorPos.set(targetX, targetY, targetZ);
    this.cursor.position.copy(this.cursorPos);

    // Emit hover event for UI/GameLoop to react
    this.dispatchEvent(
      new CustomEvent("hover", {
        detail: { voxelPosition: this.cursorPos },
      })
    );
  }

  /**
   * Gets the current cursor position.
   * @returns {THREE.Vector3} - Current cursor position
   */
  getCursorPosition() {
    return this.cursorPos;
  }

  // --- GAMEPAD LOGIC ---

  onGamepadConnected(event) {
    console.log(`Gamepad connected: ${event.gamepad.id}`);
    this.dispatchEvent(
      new CustomEvent("gamepadconnected", { detail: event.gamepad })
    );
  }

  onGamepadDisconnected(event) {
    this.gamepads.delete(event.gamepad.index);
    this.dispatchEvent(
      new CustomEvent("gamepaddisconnected", { detail: event.gamepad })
    );
  }

  /**
   * Updates gamepad state. Should be called every frame.
   */
  update() {
    const activeGamepads = navigator.getGamepads ? navigator.getGamepads() : [];

    for (const gamepad of activeGamepads) {
      if (!gamepad) continue;

      if (!this.gamepads.has(gamepad.index)) {
        this.gamepads.set(gamepad.index, {
          buttons: new Array(gamepad.buttons.length).fill(false),
          axes: new Array(gamepad.axes.length).fill(0),
        });
      }

      const prevState = this.gamepads.get(gamepad.index);

      // Buttons
      gamepad.buttons.forEach((btn, i) => {
        const pressed = btn.pressed;
        if (pressed !== prevState.buttons[i]) {
          prevState.buttons[i] = pressed;
          const eventType = pressed ? "gamepadbuttondown" : "gamepadbuttonup";
          this.dispatchEvent(
            new CustomEvent(eventType, {
              detail: { gamepadIndex: gamepad.index, buttonIndex: i },
            })
          );
        }
      });

      // Axes
      gamepad.axes.forEach((val, i) => {
        const cleanVal = Math.abs(val) > this.axisDeadzone ? val : 0;
        // Always emit axis state if non-zero (for continuous movement)
        if (Math.abs(cleanVal) > 0 || prevState.axes[i] !== 0) {
          prevState.axes[i] = cleanVal;
          this.dispatchEvent(
            new CustomEvent("gamepadaxis", {
              detail: {
                gamepadIndex: gamepad.index,
                axisIndex: i,
                value: cleanVal,
              },
            })
          );
        }
      });
    }
  }

  // --- KEYBOARD LOGIC ---

  onKeyDown(event) {
    this.keys.add(event.code);
    this.dispatchEvent(new CustomEvent("keydown", { detail: event.code }));
  }

  onKeyUp(event) {
    this.keys.delete(event.code);
    this.dispatchEvent(new CustomEvent("keyup", { detail: event.code }));
  }

  /**
   * Checks if a key is currently pressed.
   * @param {string} code - Key code
   * @returns {boolean} - True if key is pressed
   */
  isKeyPressed(code) {
    return this.keys.has(code);
  }

  // --- MOUSE LOGIC ---

  onMouseMove(event) {
    this.updateMouseCoords(event);
    const hit = this.raycast();

    if (hit) {
      // Direct set, because raycast already calculated the "voxelPosition" (target)
      this.setCursor(
        hit.voxelPosition.x,
        hit.voxelPosition.y,
        hit.voxelPosition.z
      );
    } else {
      // Optional: Hide cursor if mouse leaves grid?
      // this.cursor.visible = false;
    }
  }

  onMouseClick(event) {
    if (this.cursor.visible) {
      this.dispatchEvent(new CustomEvent("click", { detail: this.cursorPos }));
    }
  }

  updateMouseCoords(event) {
    const rect = this.domElement.getBoundingClientRect();
    this.mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
    this.mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
  }

  /**
   * Resolves a world-space point and normal into voxel coordinates.
   * Shared logic for Raycasting and potentially other inputs.
   * @param {THREE.Vector3} point - World space point
   * @param {THREE.Vector3} normal - Surface normal
   * @returns {{ hitPosition: Position; voxelPosition: Position; normal: THREE.Vector3 }} - Resolved cursor data
   */
  resolveCursorFromWorldPosition(point, normal) {
    const p = point.clone();

    // Move slightly inside the block to handle edge cases
    // (p - n * 0.5) puts us inside the block we hit
    p.addScaledVector(normal, -0.5);

    const x = Math.round(p.x);
    const y = Math.round(p.y);
    const z = Math.round(p.z);

    // Calculate the "Target" tile (e.g. the space *adjacent* to the face)
    const targetX = x + Math.round(normal.x);
    const targetY = y + Math.round(normal.y);
    const targetZ = z + Math.round(normal.z);

    return {
      hitPosition: { x, y, z }, // The solid block
      voxelPosition: { x: targetX, y: targetY, z: targetZ }, // The empty space adjacent
      normal: normal,
    };
  }

  /**
   * Performs raycast from camera through mouse position.
   * @returns {null | { hitPosition: Position; voxelPosition: Position; normal: THREE.Vector3 }} - Hit data or null
   */
  raycast() {
    this.raycaster.setFromCamera(this.mouse, this.camera);
    const intersects = this.raycaster.intersectObjects(
      this.scene.children,
      true
    );
    const hit = intersects.find((i) => i.object.isInstancedMesh);

    if (hit) {
      return this.resolveCursorFromWorldPosition(hit.point, hit.face.normal);
    }
    return null;
  }
}
