"""World — object registry and containment tree management."""

from __future__ import annotations

from h2g2.engine.game_object import GameObject, Room, Flag


class World:
    """Registry of all game objects and rooms."""

    def __init__(self) -> None:
        self.objects: dict[str, GameObject] = {}
        self.rooms: dict[str, Room] = {}

        # Root containers (created during content registration)
        self.global_objects: GameObject | None = None
        self.local_globals: GameObject | None = None
        self.protagonist: GameObject | None = None

    def register(self, obj: GameObject) -> GameObject:
        """Add an object to the registry."""
        self.objects[obj.id] = obj
        if isinstance(obj, Room):
            self.rooms[obj.id] = obj
        return obj

    def get(self, id: str) -> GameObject:
        return self.objects[id]

    def get_room(self, id: str) -> Room:
        room = self.rooms.get(id)
        if room is None:
            raise KeyError(f"Room {id!r} not found")
        return room

    def visible_objects(self, here: Room, protagonist: GameObject) -> list[GameObject]:
        """Return all objects the player can currently see/interact with.

        Search order matches ZIL: room contents, inventory, containers
        with SEARCHBIT, local globals for the room, global objects.
        """
        result: list[GameObject] = []
        seen: set[int] = set()

        def add(obj: GameObject) -> None:
            if id(obj) not in seen:
                seen.add(id(obj))
                result.append(obj)

        # 1. Room contents
        for obj in here.children:
            if Flag.INVISIBLE not in obj.flags:
                add(obj)
                # Search inside open/searchable containers
                if obj.fset_q(Flag.SEARCHBIT) or obj.fset_q(Flag.OPENBIT):
                    for child in obj.children:
                        add(child)

        # 2. Inventory
        for obj in protagonist.children:
            add(obj)
            if obj.fset_q(Flag.SEARCHBIT) or obj.fset_q(Flag.OPENBIT):
                for child in obj.children:
                    add(child)

        # 3. Local globals for this room
        for obj in here.global_objects:
            add(obj)

        # 4. Global objects
        if self.global_objects:
            for obj in self.global_objects.children:
                if Flag.INVISIBLE not in obj.flags:
                    add(obj)

        return result

    def find_by_name(
        self, name: str, adjective: str | None,
        here: Room, protagonist: GameObject,
    ) -> list[GameObject]:
        """Find objects matching a noun (and optional adjective)."""
        name = name.lower()
        adj = adjective.lower() if adjective else None
        candidates = self.visible_objects(here, protagonist)
        matches = []

        for obj in candidates:
            if name in obj.synonyms or name == obj.id.lower():
                if adj is None or adj in obj.adjectives:
                    matches.append(obj)

        # Also check room pseudo-objects
        for word, handler in here.pseudo:
            if name == word.lower():
                # Create a temporary pseudo-object
                pseudo = GameObject(
                    f"pseudo_{word}", desc=word, action=handler,
                )
                matches.append(pseudo)

        return matches
