/**
 * @typedef {import("./types.js").GridSize} GridSize
 * @typedef {import("./types.js").Position} Position
 * @typedef {import("./types.js").VoxelId} VoxelId
 * @typedef {import("./types.js").Hazard} Hazard
 * @typedef {import("./types.js").VoxelData} VoxelData
 * @typedef {import("./types.js").MoveUnitOptions} MoveUnitOptions
 * @typedef {import("../units/Unit.js").Unit} Unit
 */

/**
 * VoxelGrid.js
 * The spatial data structure for the game world.
 * Manages terrain IDs (Uint8Array) and spatial unit lookups (Map).
 * @class
 */
export class VoxelGrid {
  /**
   * @param {number} width - Grid width
   * @param {number} height - Grid height
   * @param {number} depth - Grid depth
   */
  constructor(width, height, depth) {
    /** @type {GridSize} */
    this.size = { x: width, y: height, z: depth };

    // Flat array for terrain IDs (0=Air, 1=Floor, 10=Cover, etc.)
    /** @type {Uint8Array} */
    this.cells = new Uint8Array(width * height * depth);

    // Spatial Hash for Units: "x,y,z" -> UnitObject
    /** @type {Map<string, Unit>} */
    this.unitMap = new Map();

    // Hazard Map: "x,y,z" -> { id, duration }
    /** @type {Map<string, Hazard>} */
    this.hazardMap = new Map();
  }

  // --- COORDINATE HELPERS ---
  /**
   * Generates a key string from coordinates.
   * @param {number | Position} x - X coordinate or position object
   * @param {number} [y] - Y coordinate
   * @param {number} [z] - Z coordinate
   * @returns {string} - Key string
   * @private
   */
  _key(x, y, z) {
    // Handle object input {x,y,z} or raw args
    if (typeof x === "object") return `${x.x},${x.y},${x.z}`;
    return `${x},${y},${z}`;
  }

  /**
   * Calculates array index from coordinates.
   * @param {number} x - X coordinate
   * @param {number} y - Y coordinate
   * @param {number} z - Z coordinate
   * @returns {number} - Array index
   * @private
   */
  _index(x, y, z) {
    return y * this.size.x * this.size.z + z * this.size.x + x;
  }

  /**
   * Checks if coordinates are within bounds.
   * @param {number | Position} x - X coordinate or position object
   * @param {number} [y] - Y coordinate
   * @param {number} [z] - Z coordinate
   * @returns {boolean} - True if within bounds
   */
  isValidBounds(x, y, z) {
    // Handle object input
    if (typeof x === "object") {
      z = x.z;
      y = x.y;
      x = x.x;
    }

    return (
      x >= 0 &&
      x < this.size.x &&
      y >= 0 &&
      y < this.size.y &&
      z >= 0 &&
      z < this.size.z
    );
  }

  // --- CORE VOXEL MANIPULATION ---

  /**
   * Gets the voxel ID at the specified coordinates.
   * @param {number} x - X coordinate
   * @param {number} y - Y coordinate
   * @param {number} z - Z coordinate
   * @returns {VoxelId} - Voxel ID (0 for air)
   */
  getCell(x, y, z) {
    if (!this.isValidBounds(x, y, z)) return 0; // Out of bounds is Air
    return this.cells[this._index(x, y, z)];
  }

  /**
   * Sets the voxel ID at the specified coordinates.
   * @param {number} x - X coordinate
   * @param {number} y - Y coordinate
   * @param {number} z - Z coordinate
   * @param {VoxelId} id - Voxel ID to set
   */
  setCell(x, y, z, id) {
    if (this.isValidBounds(x, y, z)) {
      this.cells[this._index(x, y, z)] = id;
    }
  }

  /**
   * Fills the entire grid with a specific ID.
   * Used by RuinGenerator to create a solid block before carving.
   * @param {VoxelId} id - Voxel ID to fill with
   */
  fill(id) {
    this.cells.fill(id);
  }

  /**
   * Creates a copy of the grid data.
   * Used by Cellular Automata for smoothing passes.
   * @returns {VoxelGrid} - Cloned grid
   */
  clone() {
    const newGrid = new VoxelGrid(this.size.x, this.size.y, this.size.z);
    newGrid.cells.set(this.cells); // Fast copy
    return newGrid;
  }

  // --- QUERY & PHYSICS ---

  /**
   * Checks if a position is solid.
   * @param {Position} pos - Position to check
   * @returns {boolean} - True if solid
   */
  isSolid(pos) {
    const id = this.getCell(pos.x, pos.y, pos.z);
    return id !== 0; // 0 is Air
  }

  /**
   * Checks if a position is occupied by a unit.
   * @param {Position} pos - Position to check
   * @returns {boolean} - True if occupied
   */
  isOccupied(pos) {
    return this.unitMap.has(this._key(pos));
  }

  /**
   * Gets the unit at a position.
   * @param {Position} pos - Position to check
   * @returns {Unit | undefined} - Unit at position or undefined
   */
  getUnitAt(pos) {
    return this.unitMap.get(this._key(pos));
  }

  /**
   * Returns true if the voxel is destructible cover (IDs 10-20).
   * @param {Position} pos - Position to check
   * @returns {boolean} - True if destructible
   */
  isDestructible(pos) {
    const id = this.getCell(pos.x, pos.y, pos.z);
    return id >= 10 && id <= 20;
  }

  /**
   * Destroys a destructible voxel.
   * @param {Position} pos - Position to destroy
   * @returns {boolean} - True if destroyed
   */
  destroyVoxel(pos) {
    if (this.isDestructible(pos)) {
      this.setCell(pos.x, pos.y, pos.z, 0); // Turn to Air
      // TODO: Trigger particle event via EventBus
      return true;
    }
    return false;
  }

  /**
   * Helper for AI to find cover or hazards.
   * Returns list of {x,y,z,id} objects within radius.
   * @param {Position} center - Center position
   * @param {number} radius - Search radius
   * @param {((id: VoxelId, x: number, y: number, z: number) => boolean) | null} [filterFn] - Optional filter function
   * @returns {VoxelData[]} - Array of voxel data
   */
  getVoxelsInRadius(center, radius, filterFn = null) {
    const results = [];
    const r = Math.ceil(radius);

    for (let x = center.x - r; x <= center.x + r; x++) {
      for (let z = center.z - r; z <= center.z + r; z++) {
        for (let y = center.y - 1; y <= center.y + 2; y++) {
          // Check varied height
          if (this.isValidBounds(x, y, z)) {
            const id = this.getCell(x, y, z);
            if (!filterFn || filterFn(id, x, y, z)) {
              results.push({ x, y, z, id });
            }
          }
        }
      }
    }
    return results;
  }

  // --- UNIT MOVEMENT ---

  /**
   * Places a unit at a position.
   * @param {Unit} unit - Unit to place
   * @param {Position} pos - Target position
   */
  placeUnit(unit, pos) {
    // Remove from old location
    if (unit.position) {
      const oldKey = this._key(unit.position);
      if (this.unitMap.get(oldKey) === unit) {
        this.unitMap.delete(oldKey);
      }
    }

    // Update Unit
    unit.position = { x: pos.x, y: pos.y, z: pos.z };

    // Add to new location
    this.unitMap.set(this._key(pos), unit);
  }

  /**
   * Moves a unit to a target position.
   * @param {Unit} unit - Unit to move
   * @param {Position} targetPos - Target position
   * @param {MoveUnitOptions} [options] - Move options
   * @returns {boolean} - True if moved successfully
   */
  moveUnit(unit, targetPos, options = {}) {
    if (!this.isValidBounds(targetPos)) return false;

    // Collision Check (can be bypassed by 'force' for Teleport/Swap)
    if (
      !options.force &&
      (this.isSolid(targetPos) || this.isOccupied(targetPos))
    ) {
      return false;
    }

    this.placeUnit(unit, targetPos);
    return true;
  }

  // --- HAZARDS ---

  /**
   * Adds a hazard at a position.
   * @param {Position} pos - Position to add hazard
   * @param {string} typeId - Hazard type ID
   * @param {number} duration - Hazard duration
   */
  addHazard(pos, typeId, duration) {
    if (this.isValidBounds(pos)) {
      this.hazardMap.set(this._key(pos), { id: typeId, duration });
    }
  }

  /**
   * Gets the hazard at a position.
   * @param {Position} pos - Position to check
   * @returns {Hazard | undefined} - Hazard data or undefined
   */
  getHazardAt(pos) {
    return this.hazardMap.get(this._key(pos));
  }
}
