"""Main game loop and PERFORM dispatch chain."""

from __future__ import annotations

from typing import Callable

from h2g2.engine.game_object import (
    GameObject, Room, Flag, Direction,
    DirectExit, ConditionalExit, GatedExit, BlockedExit,
)
from h2g2.engine.parser import Parser, ParseResult
from h2g2.engine.state import GameState
from h2g2.engine.output import Output

# Verb handlers: verb_name -> handler function
# Each handler receives (state, prso, prsi) -> bool (True = handled)
_verb_handlers: dict[str, Callable] = {}
_pre_handlers: dict[str, Callable] = {}

# Meta-verbs that don't advance the clock
META_VERBS = {
    "save", "restore", "restart", "quit", "score",
    "verbose", "brief", "superbrief", "again", "inventory",
    "diagnose",
}


def verb_handler(verb: str) -> Callable:
    """Decorator to register a default verb handler."""
    def decorator(fn: Callable) -> Callable:
        _verb_handlers[verb] = fn
        return fn
    return decorator


def pre_handler(verb: str) -> Callable:
    """Decorator to register a pre-action handler."""
    def decorator(fn: Callable) -> Callable:
        _pre_handlers[verb] = fn
        return fn
    return decorator


class GameLoop:
    """The main game loop — parse, dispatch, clock."""

    def __init__(self, state: GameState, parser: Parser) -> None:
        self.state = state
        self.parser = parser

    def run(self) -> None:
        """Run the main game loop until the game ends."""
        state = self.state
        out = state.output

        # Initial room description
        self._describe_room()
        print(out.flush(), end="")

        while state.running:
            try:
                raw = input("\n> ")
            except (EOFError, KeyboardInterrupt):
                print("\nGoodbye!")
                break

            if not raw.strip():
                continue

            result = self.parser.parse(raw, state)
            if result is None:
                out.tell("I don't understand that.\n")
                print(out.flush(), end="")
                continue

            self._execute(result)

            # Print output
            text = out.flush()
            if text:
                print(text, end="")

    def _execute(self, result: ParseResult) -> None:
        """Execute a parsed command through the dispatch chain."""
        state = self.state

        # Set parser state
        state.prsa = result.verb
        state.prso = result.direct_obj
        state.prsi = result.indirect_obj
        state.dont_flag = result.is_negated

        # Handle movement
        if result.verb == "walk" and result.direction:
            self._do_walk(result.direction)
        else:
            self._perform(result.verb, result.direct_obj, result.indirect_obj)

        # Update IT pronoun
        if result.direct_obj and result.verb not in META_VERBS:
            state.p_it_object = result.direct_obj

        # Store for AGAIN
        if result.verb not in META_VERBS:
            state.l_prsa = result.verb
            state.l_prso = result.direct_obj
            state.l_prsi = result.indirect_obj

        # Advance clock (unless meta-verb)
        if result.verb not in META_VERBS:
            state.moves += 1
            state.clock.tick_all(state)

    def _perform(
        self, verb: str,
        prso: GameObject | None, prsi: GameObject | None,
    ) -> bool:
        """Execute the verb dispatch chain. Returns True if handled."""
        state = self.state
        out = state.output

        # 1. Room M-BEG handler
        if state.here and state.here.action:
            result = state.here.action(state, "M-BEG")
            if result:
                return True

        # 2. Pre-action handler
        pre = _pre_handlers.get(verb)
        if pre:
            result = pre(state, prso, prsi)
            if result:
                return True

        # 3. Indirect object handler
        if prsi and prsi.action:
            result = prsi.action(state)
            if result:
                return True

        # 4. Direct object handler
        if prso and prso.action:
            result = prso.action(state)
            if result:
                return True

        # 5. Default verb handler
        handler = _verb_handlers.get(verb)
        if handler:
            return handler(state, prso, prsi)

        out.tell("I don't know how to do that.\n")
        return False

    def _do_walk(self, direction_name: str) -> None:
        """Handle movement in a direction."""
        state = self.state
        out = state.output

        # Check if in a vehicle/bed
        if (state.protagonist and state.protagonist.parent
                and state.protagonist.parent != state.here
                and state.protagonist.parent.fset_q(Flag.VEHBIT)):
            out.tell("You'll have to get out of "
                     f"{state.protagonist.parent.the_desc()} first.\n")
            return

        if state.lying_down:
            out.tell("You'll have to get up first.\n")
            return

        direction = Direction[direction_name]
        if state.here is None:
            return

        exit_def = state.here.exits.get(direction)
        if exit_def is None:
            out.tell("You can't go that way.\n")
            return

        if isinstance(exit_def, DirectExit):
            self._enter_room(exit_def.destination)
        elif isinstance(exit_def, ConditionalExit):
            dest = exit_def.function(state)
            if dest:
                self._enter_room(dest)
            # If function returns None, it should have printed a message
        elif isinstance(exit_def, GatedExit):
            if exit_def.door.fset_q(Flag.OPENBIT):
                self._enter_room(exit_def.destination)
            else:
                out.tell(f"The {exit_def.door.desc} is closed.\n")
        elif isinstance(exit_def, BlockedExit):
            out.tell(exit_def.message + "\n")

    def _enter_room(self, room: Room) -> None:
        """Move the player into a new room."""
        state = self.state
        state.here = room
        state.update_lit()

        # Mark as visited
        if not room.fset_q(Flag.REVISITBIT):
            room.fset(Flag.REVISITBIT)

        # Room M-ENTER handler
        if room.action:
            room.action(state, "M-ENTER")

        self._describe_room()

    @staticmethod
    def _describe_room_static(state: GameState) -> None:
        """Describe the current room (static version for external callers)."""
        GameLoop._describe_room_impl(state)

    def _describe_room(self) -> None:
        """Describe the current room."""
        GameLoop._describe_room_impl(self.state)

    @staticmethod
    def _describe_room_impl(state: GameState) -> None:
        """Describe the current room (implementation)."""
        out = state.output

        if state.here is None:
            return

        state.update_lit()

        if not state.lit:
            out.tell("It is pitch black.\n")
            return

        # Room name
        out.tell(state.here.desc)
        # Show if player is in a vehicle/bed
        if (state.protagonist and state.protagonist.parent
                and state.protagonist.parent != state.here
                and state.protagonist.parent.fset_q(Flag.VEHBIT)):
            out.tell(f", in {state.protagonist.parent.the_desc()}")
        elif state.lying_down:
            out.tell(", lying down")
        out.tell("\n")

        # Room description
        should_describe = (
            state.verbosity == 2
            or not state.here.fset_q(Flag.REVISITBIT)
            or state.prsa == "look"
        )

        if should_describe:
            # Room M-LOOK handler
            if state.here.action:
                result = state.here.action(state, "M-LOOK")
                if result:
                    GameLoop._list_room_objects_impl(state)
                    return

            if state.here.ldesc:
                out.tell(state.here.ldesc + "\n")

        GameLoop._list_room_objects_impl(state)

    @staticmethod
    def _list_room_objects_impl(state: GameState) -> None:
        """List visible objects in the current room."""
        out = state.output

        if state.here is None:
            return

        for obj in state.here.children:
            if Flag.INVISIBLE in obj.flags or Flag.NDESCBIT in obj.flags:
                continue
            if obj is state.protagonist:
                continue
            if obj.fdesc and not obj.fset_q(Flag.TOUCHBIT):
                out.tell(obj.fdesc + "\n")
            elif obj.ldesc:
                out.tell(obj.ldesc + "\n")
            else:
                out.tell(f"There is {obj.a_desc()} here.\n")
