"""L3 SPATIAL Layer

Find nearby and related content using Morton locality.
"""
from __future__ import annotations

from typing import List, Dict, Optional, Tuple

from .base import Layer, LayerID, Token
from .._core import morton_decode, morton_encode, morton_spatial_distance


class L3_Spatial(Layer):
    """L3 SPATIAL: Find nearby and related content."""

    LAYER_ID = LayerID.SPATIAL
    LAYER_NAME = "SPATIAL"
    LAYER_DESCRIPTION = "Find nearby and related content"

    def query(self, pattern: str, limit: int = 100) -> List[Token]:
        """Query by spatial pattern."""
        if "," in pattern and ":" not in pattern:
            parts = pattern.split(",")
            if len(parts) >= 2:
                try:
                    x, y = int(parts[0]), int(parts[1])
                    z = int(parts[2]) if len(parts) > 2 else 0
                    target_morton = morton_encode(x, y, z)
                    return self._find_near_morton(target_morton, limit)
                except ValueError:
                    pass

        hex_id = pattern.upper().replace("0X", "")
        return [t for t in self._memory._tokens.values()
                if t.hex_id.startswith(hex_id)][:limit]

    def _find_near_morton(self, target: int, limit: int) -> List[Token]:
        """Find tokens near a morton code."""
        scored = []
        for token in self._memory._tokens.values():
            dist = morton_spatial_distance(target, token.morton)
            scored.append((token, dist))
        scored.sort(key=lambda x: x[1])
        return [t for t, _ in scored[:limit]]

    def filter(self, tokens: List[Token], **criteria) -> List[Token]:
        """Filter by spatial criteria."""
        results = tokens

        if "near" in criteria and "radius" in criteria:
            center = criteria["near"]
            radius = criteria["radius"]
            results = [t for t in results
                      if morton_spatial_distance(center.morton, t.morton) <= radius]

        if "x_range" in criteria:
            min_x, max_x = criteria["x_range"]
            results = [t for t in results if min_x <= t.x <= max_x]

        if "y_range" in criteria:
            min_y, max_y = criteria["y_range"]
            results = [t for t in results if min_y <= t.y <= max_y]

        if "z_range" in criteria:
            min_z, max_z = criteria["z_range"]
            results = [t for t in results if min_z <= t.z <= max_z]

        return results

    def navigate(self, token: Token, direction: str) -> Optional[Token]:
        """Navigate in spatial direction."""
        vectors = {
            "north": (0, 1, 0),
            "south": (0, -1, 0),
            "east": (1, 0, 0),
            "west": (-1, 0, 0),
            "up": (0, 0, 1),
            "down": (0, 0, -1),
        }

        if direction in vectors:
            dx, dy, dz = vectors[direction]
            target_x = token.x + dx * 10
            target_y = token.y + dy * 10
            target_z = token.z + dz * 10
            target_morton = morton_encode(target_x, target_y, target_z)

            best = None
            best_dist = float('inf')
            for t in self._memory._tokens.values():
                if t == token:
                    continue
                dist = morton_spatial_distance(t.morton, target_morton)
                if dist < best_dist:
                    best_dist = dist
                    best = t
            return best

        elif direction == "nearest":
            best = None
            best_dist = float('inf')
            for t in self._memory._tokens.values():
                if t == token:
                    continue
                dist = morton_spatial_distance(t.morton, token.morton)
                if dist < best_dist:
                    best_dist = dist
                    best = t
            return best

        return None

    def distance(self, a: Token, b: Token) -> float:
        """Spatial distance via Morton codes."""
        return morton_spatial_distance(a.morton, b.morton)

    def nearby(self, token: Token, radius: int = 100) -> List[Token]:
        """Find tokens within radius."""
        results = []
        for t in self._memory._tokens.values():
            if t != token and morton_spatial_distance(token.morton, t.morton) <= radius:
                results.append(t)
        return sorted(results, key=lambda t: morton_spatial_distance(token.morton, t.morton))

    def region(self, center: Token, bounds: Tuple[int, int, int]) -> List[Token]:
        """Get all tokens in a bounding box."""
        w, h, d = bounds
        min_x, max_x = center.x - w // 2, center.x + w // 2
        min_y, max_y = center.y - h // 2, center.y + h // 2
        min_z, max_z = center.z - d // 2, center.z + d // 2

        return [t for t in self._memory._tokens.values()
                if min_x <= t.x <= max_x and
                   min_y <= t.y <= max_y and
                   min_z <= t.z <= max_z]

    def coordinates(self, token: Token) -> Tuple[int, int, int]:
        """Get (x, y, z) coordinates for token."""
        return (token.x, token.y, token.z)

    def centroid(self, tokens: List[Token]) -> Tuple[float, float, float]:
        """Compute centroid of token set."""
        if not tokens:
            return (0.0, 0.0, 0.0)

        sum_x = sum(t.x for t in tokens)
        sum_y = sum(t.y for t in tokens)
        sum_z = sum(t.z for t in tokens)
        n = len(tokens)
        return (sum_x / n, sum_y / n, sum_z / n)
