/**
 * @typedef {import("./types.js").GeneratedAssets} GeneratedAssets
 * @typedef {import("./VoxelGrid.js").VoxelGrid} VoxelGrid
 */

import * as THREE from "three";

/**
 * VoxelManager.js
 * Handles the Three.js rendering of the VoxelGrid data.
 * Updated to support Camera Focus Targeting, Emissive Textures, and Multi-Material Voxels.
 * @class
 */
export class VoxelManager {
  /**
   * @param {VoxelGrid} grid - Voxel grid to render
   * @param {THREE.Scene} scene - Three.js scene
   */
  constructor(grid, scene) {
    /** @type {VoxelGrid} */
    this.grid = grid;
    /** @type {THREE.Scene} */
    this.scene = scene;

    // Map of Voxel ID -> InstancedMesh
    /** @type {Map<number, THREE.InstancedMesh>} */
    this.meshes = new Map();

    // Default Material Definitions (Fallback)
    // Store actual Material instances, not just configs
    /** @type {Record<number, THREE.MeshStandardMaterial | THREE.MeshStandardMaterial[]>} */
    this.materials = {
      1: new THREE.MeshStandardMaterial({ color: 0x555555, roughness: 0.8 }), // Stone
      2: new THREE.MeshStandardMaterial({ color: 0x3d2817, roughness: 1.0 }), // Dirt/Floor Base
      10: new THREE.MeshStandardMaterial({ color: 0x8b4513 }), // Wood (Destructible)
      15: new THREE.MeshStandardMaterial({
        color: 0x00ffff,
        emissive: 0x004444,
      }), // Crystal
    };

    // Shared Geometry
    /** @type {THREE.BoxGeometry} */
    this.geometry = new THREE.BoxGeometry(1, 1, 1);

    // Camera Anchor: Invisible object to serve as OrbitControls target
    /** @type {THREE.Object3D} */
    this.focusTarget = new THREE.Object3D();
    this.focusTarget.name = "CameraFocusTarget";
    this.scene.add(this.focusTarget);

    // Highlight tracking (managed externally, but VoxelManager provides helper methods)
    // These will be Set<THREE.Mesh> passed from GameLoop
    /** @type {Set<THREE.Mesh> | null} */
    this.rangeHighlights = null;
    /** @type {Set<THREE.Mesh> | null} */
    this.aoeReticle = null;
  }

  /**
   * Updates the material definitions with generated assets.
   * Supports both simple Canvas textures and complex {diffuse, emissive, normal, roughness, bump} objects.
   * NOW SUPPORTS: 'palette' for batch loading procedural variations.
   * @param {GeneratedAssets} assets - Generated assets from world generator
   */
  updateMaterials(assets) {
    if (!assets) return;

    // Helper to create a material INSTANCE from an asset
    const createMaterial = (asset) => {
      if (!asset) return null;

      const matDef = {
        color: 0xffffff, // White base to show texture colors
        roughness: 0.8,
      };

      // Check if it's a simple Canvas or a Composite Object
      if (
        asset instanceof OffscreenCanvas ||
        asset instanceof HTMLCanvasElement
      ) {
        // Simple Diffuse Map
        const tex = new THREE.CanvasTexture(asset);
        tex.magFilter = THREE.NearestFilter;
        tex.minFilter = THREE.NearestFilter;
        tex.colorSpace = THREE.SRGBColorSpace;
        matDef.map = tex;
      } else if (asset.diffuse) {
        // Complex Map (Diffuse + optional maps)
        const diffTex = new THREE.CanvasTexture(asset.diffuse);
        diffTex.magFilter = THREE.NearestFilter;
        diffTex.minFilter = THREE.NearestFilter;
        diffTex.colorSpace = THREE.SRGBColorSpace;
        matDef.map = diffTex;

        // Emissive Map
        if (asset.emissive) {
          const emTex = new THREE.CanvasTexture(asset.emissive);
          emTex.magFilter = THREE.NearestFilter;
          emTex.minFilter = THREE.NearestFilter;
          emTex.colorSpace = THREE.SRGBColorSpace;

          matDef.emissiveMap = emTex;
          matDef.emissive = 0xffffff; // Max brightness for the map values
          matDef.emissiveIntensity = 1.0; // Adjustable glow strength
        }

        // Normal Map
        if (asset.normal) {
          const normTex = new THREE.CanvasTexture(asset.normal);
          normTex.magFilter = THREE.NearestFilter;
          normTex.minFilter = THREE.NearestFilter;
          normTex.colorSpace = THREE.NoColorSpace; // Normal maps are data, linear color space
          matDef.normalMap = normTex;
        }

        // Roughness Map
        if (asset.roughness) {
          const roughTex = new THREE.CanvasTexture(asset.roughness);
          roughTex.magFilter = THREE.NearestFilter;
          roughTex.minFilter = THREE.NearestFilter;
          roughTex.colorSpace = THREE.NoColorSpace;
          matDef.roughnessMap = roughTex;
          matDef.roughness = 1.0; // Let the map drive the value
        }

        // Bump Map
        if (asset.bump) {
          const bumpTex = new THREE.CanvasTexture(asset.bump);
          bumpTex.magFilter = THREE.NearestFilter;
          bumpTex.minFilter = THREE.NearestFilter;
          bumpTex.colorSpace = THREE.NoColorSpace;
          matDef.bumpMap = bumpTex;
          matDef.bumpScale = 0.05; // Standard bump scale for voxels
        }
      }
      return new THREE.MeshStandardMaterial(matDef);
    };

    // 1. Process Standard Single Textures (Legacy/Base)
    const floorMat = assets.textures
      ? createMaterial(assets.textures["floor"])
      : null;
    const wallMat = assets.textures
      ? createMaterial(assets.textures["wall"])
      : null;

    // ID 1: Wall (All faces same)
    if (wallMat) this.materials[1] = wallMat;

    // ID 2: Floor (Top = Floor Tex, Sides/Bottom = Wall Tex)
    if (floorMat) {
      // Use wall texture for sides if available.
      // If not, fallback to the existing definition for ID 1 (Stone) to ensure sides look like walls.
      let sideMat = wallMat;

      if (!sideMat && this.materials[1] && !Array.isArray(this.materials[1])) {
        sideMat = this.materials[1];
      }

      // Fallback to floor material if absolutely nothing else exists
      if (!sideMat) sideMat = floorMat;

      // BoxGeometry Material Index Order: Right, Left, Top, Bottom, Front, Back
      this.materials[2] = [
        sideMat, // Right (+x)
        sideMat, // Left (-x)
        floorMat, // Top (+y) - The Damp Cave Floor
        sideMat, // Bottom (-y)
        sideMat, // Front (+z)
        sideMat, // Back (-z)
      ];
    }

    // 2. Process Palette (Procedural Variations)
    if (assets.palette) {
      // First pass: Create all materials from palette assets
      for (const [idStr, asset] of Object.entries(assets.palette)) {
        const id = parseInt(idStr);
        const mat = createMaterial(asset);
        if (mat) {
          this.materials[id] = mat;
        }
      }

      // Second pass: Organize multi-material arrays for floors
      for (const [idStr, asset] of Object.entries(assets.palette)) {
        const id = parseInt(idStr);

        // Logic for Floor Variations (IDs 200-299)
        if (id >= 200 && id <= 299) {
          const floorMat = this.materials[id];

          // Attempt to find matching wall variation (e.g. Floor 205 -> Wall 105)
          // If missing, fallback to Base Wall (ID 1)
          let sideMat = this.materials[id - 100];

          if (
            !sideMat &&
            this.materials[1] &&
            !Array.isArray(this.materials[1])
          ) {
            sideMat = this.materials[1];
          }

          // Fallback to floor material itself if absolutely nothing else exists
          if (!sideMat) sideMat = floorMat;

          if (sideMat && floorMat) {
            this.materials[id] = [
              sideMat,
              sideMat,
              floorMat,
              sideMat,
              sideMat,
              sideMat,
            ];
          }
        }
      }
    }

    this.update();
  }

  /**
   * Re-calculates meshes based on grid data.
   */
  update() {
    // 1. Cleanup existing meshes
    this.meshes.forEach((mesh) => {
      this.scene.remove(mesh);

      // Handle cleanup for single material or array of materials
      if (Array.isArray(mesh.material)) {
        mesh.material.forEach((m) => {
          // Dispose textures if they are unique to this mesh instance
          // (But here we share them via this.materials, so be careful not to over-dispose if reused)
          // For now, assume simple cleanup
          // if (m.map) m.map.dispose();
          // m.dispose();
        });
      } else {
        // if (mesh.material.map) mesh.material.map.dispose();
        // mesh.material.dispose();
      }

      // Note: Mesh objects don't have a dispose() method, only geometry and material do
    });
    this.meshes.clear();

    // 2. Bucket coordinates by Voxel ID
    const buckets = {};
    const dummy = new THREE.Object3D();

    for (let x = 0; x < this.grid.size.x; x++) {
      for (let y = 0; y < this.grid.size.y; y++) {
        for (let z = 0; z < this.grid.size.z; z++) {
          const id = this.grid.getCell(x, y, z);

          if (id !== 0) {
            if (!buckets[id]) buckets[id] = [];

            dummy.position.set(x, y, z);
            dummy.updateMatrix();
            buckets[id].push(dummy.matrix.clone());
          }
        }
      }
    }

    // 3. Generate InstancedMesh for each ID
    for (const [idStr, matrices] of Object.entries(buckets)) {
      const id = parseInt(idStr);
      const count = matrices.length;

      // Get the actual Material Instance (or Array of Instances)
      const material =
        this.materials[id] ||
        new THREE.MeshStandardMaterial({ color: 0xff00ff });

      const mesh = new THREE.InstancedMesh(this.geometry, material, count);

      matrices.forEach((mat, index) => {
        mesh.setMatrixAt(index, mat);
      });

      mesh.instanceMatrix.needsUpdate = true;
      this.meshes.set(id, mesh);
      this.scene.add(mesh);
    }

    // 4. Update Focus Target to Center of Grid
    // We position it at the horizontal center, and slightly up (y=0 or 1)
    this.focusTarget.position.set(
      this.grid.size.x / 2,
      0,
      this.grid.size.z / 2
    );
  }

  /**
   * Helper to center the camera view on the grid.
   * @param {import("three/examples/jsm/controls/OrbitControls.js").OrbitControls} controls - The OrbitControls instance to update
   */
  focusCamera(controls) {
    if (controls && this.focusTarget) {
      controls.target.copy(this.focusTarget.position);
      controls.update();
    }
  }

  /**
   * Updates a single voxel (triggers full update).
   * @param {number} x - X coordinate
   * @param {number} y - Y coordinate
   * @param {number} z - Z coordinate
   */
  updateVoxel(x, y, z) {
    this.update();
  }

  /**
   * Sets the highlight tracking sets (called from GameLoop).
   * @param {Set<THREE.Mesh>} rangeHighlights - Set to track range highlight meshes
   * @param {Set<THREE.Mesh>} aoeReticle - Set to track AoE reticle meshes
   */
  setHighlightSets(rangeHighlights, aoeReticle) {
    this.rangeHighlights = rangeHighlights;
    this.aoeReticle = aoeReticle;
  }

  /**
   * Highlights tiles within skill range (red outline).
   * @param {import("./types.js").Position} sourcePos - Source position
   * @param {number} range - Skill range in tiles
   * @param {string} style - Highlight style (e.g., 'RED_OUTLINE')
   */
  highlightRange(sourcePos, range, style = "RED_OUTLINE") {
    if (!this.rangeHighlights) return;

    // Clear existing range highlights
    this.clearRangeHighlights();

    // Generate all tiles within Manhattan distance range
    const tiles = [];
    for (let x = sourcePos.x - range; x <= sourcePos.x + range; x++) {
      for (let y = sourcePos.y - range; y <= sourcePos.y + range; y++) {
        for (let z = sourcePos.z - range; z <= sourcePos.z + range; z++) {
          const dist =
            Math.abs(x - sourcePos.x) +
            Math.abs(y - sourcePos.y) +
            Math.abs(z - sourcePos.z);
          if (dist <= range) {
            // Check if position is valid and walkable
            if (this.grid.isValidBounds({ x, y, z })) {
              tiles.push({ x, y, z });
            }
          }
        }
      }
    }

    // Create red outline materials (similar to movement highlights but red)
    const outerGlowMaterial = new THREE.LineBasicMaterial({
      color: 0x660000,
      transparent: true,
      opacity: 0.3,
    });

    const midGlowMaterial = new THREE.LineBasicMaterial({
      color: 0x880000,
      transparent: true,
      opacity: 0.5,
    });

    const highlightMaterial = new THREE.LineBasicMaterial({
      color: 0xff0000, // Bright red
      transparent: true,
      opacity: 1.0,
    });

    const thickMaterial = new THREE.LineBasicMaterial({
      color: 0xcc0000,
      transparent: true,
      opacity: 0.8,
    });

    // Create base plane geometry
    const baseGeometry = new THREE.PlaneGeometry(1, 1);
    baseGeometry.rotateX(-Math.PI / 2);

    // Create highlights for each tile
    tiles.forEach((pos) => {
      // Find walkable Y level (similar to movement highlights)
      let walkableY = pos.y;
      // Check if there's a floor at this position
      if (this.grid.getCell(pos.x, pos.y - 1, pos.z) === 0) {
        // No floor, try to find walkable level
        for (let checkY = pos.y; checkY >= 0; checkY--) {
          if (this.grid.getCell(pos.x, checkY - 1, pos.z) !== 0) {
            walkableY = checkY;
            break;
          }
        }
      }

      const floorSurfaceY = walkableY - 0.5;

      // Outer glow
      const outerGlowGeometry = new THREE.PlaneGeometry(1.15, 1.15);
      outerGlowGeometry.rotateX(-Math.PI / 2);
      const outerGlowEdges = new THREE.EdgesGeometry(outerGlowGeometry);
      const outerGlowLines = new THREE.LineSegments(
        outerGlowEdges,
        outerGlowMaterial
      );
      outerGlowLines.position.set(pos.x, floorSurfaceY + 0.003, pos.z);
      this.scene.add(outerGlowLines);
      this.rangeHighlights.add(outerGlowLines);

      // Mid glow
      const midGlowGeometry = new THREE.PlaneGeometry(1.08, 1.08);
      midGlowGeometry.rotateX(-Math.PI / 2);
      const midGlowEdges = new THREE.EdgesGeometry(midGlowGeometry);
      const midGlowLines = new THREE.LineSegments(
        midGlowEdges,
        midGlowMaterial
      );
      midGlowLines.position.set(pos.x, floorSurfaceY + 0.002, pos.z);
      this.scene.add(midGlowLines);
      this.rangeHighlights.add(midGlowLines);

      // Thick outline
      const thickGeometry = new THREE.PlaneGeometry(1.02, 1.02);
      thickGeometry.rotateX(-Math.PI / 2);
      const thickEdges = new THREE.EdgesGeometry(thickGeometry);
      const thickLines = new THREE.LineSegments(thickEdges, thickMaterial);
      thickLines.position.set(pos.x, floorSurfaceY + 0.001, pos.z);
      this.scene.add(thickLines);
      this.rangeHighlights.add(thickLines);

      // Main bright outline
      const edgesGeometry = new THREE.EdgesGeometry(baseGeometry);
      const lineSegments = new THREE.LineSegments(
        edgesGeometry,
        highlightMaterial
      );
      lineSegments.position.set(pos.x, floorSurfaceY, pos.z);
      this.scene.add(lineSegments);
      this.rangeHighlights.add(lineSegments);
    });
  }

  /**
   * Shows AoE reticle (solid red highlight) for affected tiles.
   * @param {import("./types.js").Position[]} tiles - Array of tile positions in AoE
   */
  showReticle(tiles) {
    if (!this.aoeReticle) return;

    // Clear existing reticle
    this.clearReticle();

    // Create solid red material (more opaque than outline)
    const reticleMaterial = new THREE.MeshStandardMaterial({
      color: 0xff0000,
      transparent: true,
      opacity: 0.4,
      emissive: 0x330000,
      emissiveIntensity: 0.5,
    });

    // Create plane geometry for solid highlight
    const planeGeometry = new THREE.PlaneGeometry(1, 1);
    planeGeometry.rotateX(-Math.PI / 2);

    // Create solid highlights for each tile
    tiles.forEach((pos) => {
      // Find walkable Y level
      let walkableY = pos.y;
      if (this.grid.getCell(pos.x, pos.y - 1, pos.z) === 0) {
        for (let checkY = pos.y; checkY >= 0; checkY--) {
          if (this.grid.getCell(pos.x, checkY - 1, pos.z) !== 0) {
            walkableY = checkY;
            break;
          }
        }
      }

      const floorSurfaceY = walkableY - 0.5;

      // Create solid plane mesh
      const plane = new THREE.Mesh(planeGeometry, reticleMaterial);
      plane.position.set(pos.x, floorSurfaceY + 0.01, pos.z);
      this.scene.add(plane);
      this.aoeReticle.add(plane);
    });
  }

  /**
   * Clears all range highlights.
   */
  clearRangeHighlights() {
    if (!this.rangeHighlights) return;
    this.rangeHighlights.forEach((mesh) => {
      this.scene.remove(mesh);
      if (mesh.geometry) mesh.geometry.dispose();
      if (mesh.material) {
        if (Array.isArray(mesh.material)) {
          mesh.material.forEach((m) => m.dispose());
        } else {
          mesh.material.dispose();
        }
      }
      // Note: Mesh objects don't have a dispose() method, only geometry and material do
    });
    this.rangeHighlights.clear();
  }

  /**
   * Clears AoE reticle.
   */
  clearReticle() {
    if (!this.aoeReticle) return;
    this.aoeReticle.forEach((mesh) => {
      this.scene.remove(mesh);
      if (mesh.geometry) mesh.geometry.dispose();
      if (mesh.material) {
        if (Array.isArray(mesh.material)) {
          mesh.material.forEach((m) => m.dispose());
        } else {
          mesh.material.dispose();
        }
      }
      // Note: Mesh objects don't have a dispose() method, only geometry and material do
    });
    this.aoeReticle.clear();
  }

  /**
   * Highlights specific tiles with obstruction-based dimming.
   * @param {Array<{pos: import("./types.js").Position, obstruction: number}>} tilesWithObstruction - Array of tile positions with obstruction levels (0-1)
   * @param {string} style - Highlight style (e.g., 'RED_OUTLINE')
   */
  highlightTilesWithObstruction(tilesWithObstruction, style = "RED_OUTLINE") {
    if (!this.rangeHighlights) return;

    // Clear existing range highlights
    this.clearRangeHighlights();

    // Create highlights for each tile with obstruction-based opacity
    tilesWithObstruction.forEach(({ pos, obstruction }) => {
      // Calculate opacity based on obstruction (0 obstruction = full brightness, 1 obstruction = dim)
      // Success chance = 1 - obstruction, so opacity should reflect that
      const baseOpacity = 1.0;
      const dimmedOpacity = baseOpacity * (1 - obstruction * 0.7); // Dim up to 70% based on obstruction
      
      // Create materials with obstruction-based opacity
      const outerGlowMaterial = new THREE.LineBasicMaterial({
        color: 0x660000,
        transparent: true,
        opacity: 0.3 * dimmedOpacity,
      });

      const midGlowMaterial = new THREE.LineBasicMaterial({
        color: 0x880000,
        transparent: true,
        opacity: 0.5 * dimmedOpacity,
      });

      const highlightMaterial = new THREE.LineBasicMaterial({
        color: 0xff0000, // Bright red
        transparent: true,
        opacity: dimmedOpacity,
      });

      const thickMaterial = new THREE.LineBasicMaterial({
        color: 0xcc0000,
        transparent: true,
        opacity: 0.8 * dimmedOpacity,
      });

      // Find walkable Y level (similar to movement highlights)
      let walkableY = pos.y;
      // Check if there's a floor at this position
      if (this.grid.getCell(pos.x, pos.y - 1, pos.z) === 0) {
        // No floor, try to find walkable level
        for (let checkY = pos.y; checkY >= 0; checkY--) {
          if (this.grid.getCell(pos.x, checkY - 1, pos.z) !== 0) {
            walkableY = checkY;
            break;
          }
        }
      }

      const floorSurfaceY = walkableY - 0.5;

      // Outer glow
      const outerGlowGeometry = new THREE.PlaneGeometry(1.15, 1.15);
      outerGlowGeometry.rotateX(-Math.PI / 2);
      const outerGlowEdges = new THREE.EdgesGeometry(outerGlowGeometry);
      const outerGlowLines = new THREE.LineSegments(
        outerGlowEdges,
        outerGlowMaterial
      );
      outerGlowLines.position.set(pos.x, floorSurfaceY + 0.003, pos.z);
      this.scene.add(outerGlowLines);
      this.rangeHighlights.add(outerGlowLines);

      // Mid glow
      const midGlowGeometry = new THREE.PlaneGeometry(1.08, 1.08);
      midGlowGeometry.rotateX(-Math.PI / 2);
      const midGlowEdges = new THREE.EdgesGeometry(midGlowGeometry);
      const midGlowLines = new THREE.LineSegments(
        midGlowEdges,
        midGlowMaterial
      );
      midGlowLines.position.set(pos.x, floorSurfaceY + 0.004, pos.z);
      this.scene.add(midGlowLines);
      this.rangeHighlights.add(midGlowLines);

      // Main highlight
      const baseGeometry = new THREE.PlaneGeometry(1, 1);
      baseGeometry.rotateX(-Math.PI / 2);
      const highlightEdges = new THREE.EdgesGeometry(baseGeometry);
      const highlightLines = new THREE.LineSegments(
        highlightEdges,
        highlightMaterial
      );
      highlightLines.position.set(pos.x, floorSurfaceY + 0.005, pos.z);
      this.scene.add(highlightLines);
      this.rangeHighlights.add(highlightLines);

      // Thick border
      const thickGeometry = new THREE.PlaneGeometry(1.02, 1.02);
      thickGeometry.rotateX(-Math.PI / 2);
      const thickEdges = new THREE.EdgesGeometry(thickGeometry);
      const thickLines = new THREE.LineSegments(thickEdges, thickMaterial);
      thickLines.position.set(pos.x, floorSurfaceY + 0.006, pos.z);
      this.scene.add(thickLines);
      this.rangeHighlights.add(thickLines);
    });
  }

  /**
   * Highlights specific tiles (used for skill targeting with validation).
   * @param {import("./types.js").Position[]} tiles - Array of tile positions to highlight
   * @param {string} style - Highlight style (e.g., 'RED_OUTLINE')
   */
  highlightTiles(tiles, style = "RED_OUTLINE") {
    if (!this.rangeHighlights) return;

    // Clear existing range highlights
    this.clearRangeHighlights();

    // Create red outline materials (similar to movement highlights but red)
    const outerGlowMaterial = new THREE.LineBasicMaterial({
      color: 0x660000,
      transparent: true,
      opacity: 0.3,
    });

    const midGlowMaterial = new THREE.LineBasicMaterial({
      color: 0x880000,
      transparent: true,
      opacity: 0.5,
    });

    const highlightMaterial = new THREE.LineBasicMaterial({
      color: 0xff0000, // Bright red
      transparent: true,
      opacity: 1.0,
    });

    const thickMaterial = new THREE.LineBasicMaterial({
      color: 0xcc0000,
      transparent: true,
      opacity: 0.8,
    });

    // Create base plane geometry
    const baseGeometry = new THREE.PlaneGeometry(1, 1);
    baseGeometry.rotateX(-Math.PI / 2);

    // Create highlights for each tile
    tiles.forEach((pos) => {
      // Find walkable Y level (similar to movement highlights)
      let walkableY = pos.y;
      // Check if there's a floor at this position
      if (this.grid.getCell(pos.x, pos.y - 1, pos.z) === 0) {
        // No floor, try to find walkable level
        for (let checkY = pos.y; checkY >= 0; checkY--) {
          if (this.grid.getCell(pos.x, checkY - 1, pos.z) !== 0) {
            walkableY = checkY;
            break;
          }
        }
      }

      const floorSurfaceY = walkableY - 0.5;

      // Outer glow
      const outerGlowGeometry = new THREE.PlaneGeometry(1.15, 1.15);
      outerGlowGeometry.rotateX(-Math.PI / 2);
      const outerGlowEdges = new THREE.EdgesGeometry(outerGlowGeometry);
      const outerGlowLines = new THREE.LineSegments(
        outerGlowEdges,
        outerGlowMaterial
      );
      outerGlowLines.position.set(pos.x, floorSurfaceY + 0.003, pos.z);
      this.scene.add(outerGlowLines);
      this.rangeHighlights.add(outerGlowLines);

      // Mid glow
      const midGlowGeometry = new THREE.PlaneGeometry(1.08, 1.08);
      midGlowGeometry.rotateX(-Math.PI / 2);
      const midGlowEdges = new THREE.EdgesGeometry(midGlowGeometry);
      const midGlowLines = new THREE.LineSegments(
        midGlowEdges,
        midGlowMaterial
      );
      midGlowLines.position.set(pos.x, floorSurfaceY + 0.004, pos.z);
      this.scene.add(midGlowLines);
      this.rangeHighlights.add(midGlowLines);

      // Main highlight
      const highlightEdges = new THREE.EdgesGeometry(baseGeometry);
      const highlightLines = new THREE.LineSegments(
        highlightEdges,
        highlightMaterial
      );
      highlightLines.position.set(pos.x, floorSurfaceY + 0.005, pos.z);
      this.scene.add(highlightLines);
      this.rangeHighlights.add(highlightLines);

      // Thick border
      const thickGeometry = new THREE.PlaneGeometry(1.02, 1.02);
      thickGeometry.rotateX(-Math.PI / 2);
      const thickEdges = new THREE.EdgesGeometry(thickGeometry);
      const thickLines = new THREE.LineSegments(thickEdges, thickMaterial);
      thickLines.position.set(pos.x, floorSurfaceY + 0.006, pos.z);
      this.scene.add(thickLines);
      this.rangeHighlights.add(thickLines);
    });
  }

  /**
   * Clears all highlights (range and AoE).
   */
  clearHighlights() {
    this.clearRangeHighlights();
    this.clearReticle();
  }
}
