"""Game object model — items, rooms, NPCs, and the flag/direction system."""

from __future__ import annotations

import enum
from dataclasses import dataclass, field
from typing import Callable, Any


class Flag(enum.Enum):
    TAKEBIT = enum.auto()
    OPENBIT = enum.auto()
    TOUCHBIT = enum.auto()
    ONBIT = enum.auto()
    TRANSBIT = enum.auto()
    WEARBIT = enum.auto()
    WORNBIT = enum.auto()
    RLANDBIT = enum.auto()
    NDESCBIT = enum.auto()
    INVISIBLE = enum.auto()
    LIGHTBIT = enum.auto()
    CONTBIT = enum.auto()
    SURFACEBIT = enum.auto()
    ACTORBIT = enum.auto()
    VEHBIT = enum.auto()
    DOORBIT = enum.auto()
    READBIT = enum.auto()
    DRINKBIT = enum.auto()
    NARTICLEBIT = enum.auto()
    VOWELBIT = enum.auto()
    SEARCHBIT = enum.auto()
    TRYTAKEBIT = enum.auto()
    DARKBIT = enum.auto()
    REVISITBIT = enum.auto()
    MUNGEDBIT = enum.auto()
    OUTSIDEBIT = enum.auto()
    TOOLBIT = enum.auto()
    INTEGRALBIT = enum.auto()


class Direction(enum.Enum):
    NORTH = "north"
    NE = "northeast"
    EAST = "east"
    SE = "southeast"
    SOUTH = "south"
    SW = "southwest"
    WEST = "west"
    NW = "northwest"
    UP = "up"
    DOWN = "down"
    IN = "in"
    OUT = "out"


@dataclass
class DirectExit:
    """Simple exit to a fixed room."""
    destination: Room


@dataclass
class ConditionalExit:
    """Exit gated by a function that returns a Room or None."""
    function: Callable  # (state) -> Room | None; None means blocked


@dataclass
class GatedExit:
    """Exit that requires a door object to be open."""
    destination: Room
    door: GameObject


@dataclass
class BlockedExit:
    """Permanently blocked exit with a message."""
    message: str


Exit = DirectExit | ConditionalExit | GatedExit | BlockedExit


class GameObject:
    """A game entity — item, NPC, abstract concept, or container."""

    def __init__(
        self,
        id: str,
        *,
        desc: str = "",
        ldesc: str | None = None,
        fdesc: str | None = None,
        synonyms: list[str] | None = None,
        adjectives: list[str] | None = None,
        flags: set[Flag] | None = None,
        size: int = 5,
        capacity: int = 0,
        action: Callable | None = None,
        text: str | None = None,
    ):
        self.id = id
        self.desc = desc
        self.ldesc = ldesc
        self.fdesc = fdesc
        self.synonyms: list[str] = [s.lower() for s in (synonyms or [])]
        self.adjectives: list[str] = [a.lower() for a in (adjectives or [])]
        self.flags: set[Flag] = flags or set()
        self.size = size
        self.capacity = capacity
        self.action = action
        self.text = text

        # Containment tree
        self.parent: GameObject | None = None
        self.children: list[GameObject] = []

    def move_to(self, new_parent: GameObject | None) -> None:
        """Move this object into a new container."""
        if self.parent:
            self.parent.children.remove(self)
        self.parent = new_parent
        if new_parent:
            new_parent.children.append(self)

    def contains(self, obj: GameObject) -> bool:
        """Check if obj is a direct child."""
        return obj in self.children

    def is_held_by(self, holder: GameObject) -> bool:
        """Check if this object is carried by holder (direct or nested)."""
        current = self.parent
        while current:
            if current is holder:
                return True
            current = current.parent
        return False

    def contents_string(self) -> list[GameObject]:
        """Return visible children (not INVISIBLE or NDESCBIT)."""
        return [
            c for c in self.children
            if Flag.INVISIBLE not in c.flags and Flag.NDESCBIT not in c.flags
        ]

    def fset(self, flag: Flag) -> None:
        self.flags.add(flag)

    def fclear(self, flag: Flag) -> None:
        self.flags.discard(flag)

    def fset_q(self, flag: Flag) -> bool:
        return flag in self.flags

    def article(self) -> str:
        """Return the appropriate article for this object."""
        if Flag.NARTICLEBIT in self.flags:
            return ""
        if Flag.VOWELBIT in self.flags:
            return "an "
        return "a "

    def the_desc(self) -> str:
        """Return 'the <desc>' or just desc for proper nouns."""
        if Flag.NARTICLEBIT in self.flags:
            return self.desc
        return f"the {self.desc}"

    def a_desc(self) -> str:
        """Return 'a/an <desc>' or just desc for proper nouns."""
        if Flag.NARTICLEBIT in self.flags:
            return self.desc
        return f"{self.article()}{self.desc}"

    def __repr__(self) -> str:
        return f"<{self.__class__.__name__} {self.id!r}>"


class Room(GameObject):
    """A location in the game world."""

    def __init__(
        self,
        id: str,
        *,
        desc: str = "",
        ldesc: str | None = None,
        synonyms: list[str] | None = None,
        adjectives: list[str] | None = None,
        flags: set[Flag] | None = None,
        action: Callable | None = None,
        exits: dict[Direction, Exit] | None = None,
        global_objects: list[GameObject] | None = None,
        pseudo: list[tuple[str, Callable]] | None = None,
    ):
        super().__init__(
            id, desc=desc, ldesc=ldesc, synonyms=synonyms,
            adjectives=adjectives, flags=flags, action=action,
        )
        self.exits: dict[Direction, Exit] = exits or {}
        self.global_objects: list[GameObject] = global_objects or []
        self.pseudo: list[tuple[str, Callable]] = pseudo or []
