"""
Commands for the Evscaperoom. This contains all in-room commands as well as
admin debug-commands to help development.

Gameplay commands

- `look` - custom look
- `focus` - focus on object (to perform actions on it)
- `<action> <obj>` - arbitrary interaction with focused object
- `stand` - stand on the floor, resetting any position
- `emote` - free-form emote
- `say/whisper/shout` - simple communication

Other commands

- `evscaperoom` - starts the evscaperoom top-level menu
- `help` - custom in-room help command
- `options` - set game/accessibility options
- `who` - show who's in the room with you
- `quit` - leave a room, return to menu

Admin/development commands

- `jumpstate` - jump to specific room state
- `flag` - assign a flag to an object
- `createobj` - create a room-object set up for Evscaperoom

"""

import re

from django.conf import settings

from evennia import (
    SESSION_HANDLER,
    CmdSet,
    Command,
    InterruptCommand,
    default_cmds,
    syscmdkeys,
)
from evennia.utils import variable_from_module

from .utils import create_evscaperoom_object

_AT_SEARCH_RESULT = variable_from_module(*settings.SEARCH_AT_RESULT.rsplit(".", 1))

_RE_ARGSPLIT = re.compile(r"\s(with|on|to|in|at)\s", re.I + re.U)
_RE_EMOTE_SPEECH = re.compile(r"(\".*?\")|(\'.*?\')")
_RE_EMOTE_NAME = re.compile(r"(/\w+)")
_RE_EMOTE_PROPER_END = re.compile(r"\.$|\.[\'\"]$|\![\'\"]$|\?[\'\"]$")


# configurable help

if hasattr(settings, "EVSCAPEROOM_HELP_SUMMARY_TEXT"):
    _HELP_SUMMARY_TEXT = settings.EVSCAPEROOM_HELP_SUMMARY_TEXT
else:
    _HELP_SUMMARY_TEXT = """
   |yWhat to do ...|n
    - Your goal is to |wescape|n the room. To do that you need to |wlook|n at
      your surroundings for clues on how to escape. When you find something
      interesting, |wexamine|n it for any actions you could take with it.
   |yHow to explore ...|n
    - |whelp [obj or command]|n           - get usage help (never puzzle-related)
    - |woptions|n                         - set game/accessibility options
    - |wlook/l [obj]|n                    - give things a cursory look.
    - |wexamine/ex/e [obj]|n              - look closer at an object. Use again to
                                        look away.
    - |wstand|n                           - stand up if you were sitting, lying etc.
   |yHow to express yourself ...|n
    - |wwho [all]|n                       - show who's in the room or on server.
    - |wemote/pose/: <something>|n        - free-form emote. Use /me to refer
                                        to yourself and /name to refer to other
                                        things/players. Use quotes "..." to speak.
    - |wsay/; <something>|n               - quick-speak your mind
    - |wwhisper <something>|n             - whisper your mind
    - |wshout <something>|n               - shout your mind
   |yHow to quit like a little baby ...|n
    - |wquit / give up|n                  - admit defeat and give up
"""

_HELP_FALLBACK_TEXT = """
There is no help to be had about |y{this}|n. To look away, use |wexamine|n on
its own or with another object you are interested in.
"""

_QUIT_WARNING = """
|rDo you really want to quit?|n

{warning}

Enter |w'quit'|n again to truly give up.
"""

_QUIT_WARNING_CAN_COME_BACK = """
(Since you are not the last person to leave this room, you |gcan|n get back in here
by joining room '|c{roomname}|n' from the menu. Note however that if you leave
now, any personal achievements you may have gathered so far will be |rlost|n!)
"""

_QUIT_WARNING_LAST_CHAR = """
(You are the |rlast|n player to leave this room ('|c{roomname}|n'). This means that when you
leave, this room will go away and you |rwon't|n be able to come back to it!)
"""


class CmdEvscapeRoom(Command):
    """
    Base command parent for all Evscaperoom commands.

    This operates on the premise of 'focus' - the 'focus'
    is set on the caller, then subsequent commands will
    operate on that focus. If no focus is set,
    the operation will be general or on the room.

    Syntax:

       command [<obj1>|<arg1>] [<prep> <obj2>|<arg2>]

    """

    # always separate the command from any args with a space
    arg_regex = r"(/\w+?(\s|$))|\s|$"
    help_category = "Evscaperoom"

    # these flags allow child classes to determine how strict parsing for obj1/obj2 should be
    # (if they are given at all):
    # True - obj1/obj2 must be found as Objects, otherwise it's an error aborting command
    # False - obj1/obj2 will remain None, instead self.arg1, arg2 will be stored as strings
    # None - if obj1/obj2 are found as Objects, set them, otherwise set arg1/arg2 as strings
    obj1_search = None
    obj2_search = None

    def _search(self, query, required):
        """
        This implements the various search modes

        Args:
            query (str): The search query
            required (bool or None): This defines if query *must* be
                found to match a single local Object or not. If None,
                a non-match means returning the query unchanged. When
                False, immediately return the query. If required is False,
                don't search at all.
        Return:
            match (Object or str): The match or the search string depending
                on the `required` mode.
        Raises:
            InterruptCommand: Aborts the command quietly.
        Notes:
            The _AT_SEARCH_RESULT function will handle all error messaging
            for us.

        """
        if required is False:
            return None, query

        matches = self.caller.search(query, quiet=True)

        if not matches or len(matches) > 1:
            if required:
                if not query:
                    self.caller.msg("You must give an argument.")
                else:
                    _AT_SEARCH_RESULT(matches, self.caller, query=query)
                raise InterruptCommand
            else:
                return None, query
        else:
            return matches[0], None

    def parse(self):
        """
        Parse incoming arguments for use in all child classes.

        """
        caller = self.caller
        self.args = self.args.strip()

        # splits to either ['obj'] or e.g. ['obj', 'on', 'obj']
        parts = [part.strip() for part in _RE_ARGSPLIT.split(" " + self.args, 1)]
        nparts = len(parts)
        self.obj1 = None
        self.arg1 = None
        self.prep = None
        self.obj2 = None
        self.arg2 = None
        if nparts == 1:
            self.obj1, self.arg1 = self._search(parts[0], self.obj1_search)
        elif nparts == 3:
            obj1, self.prep, obj2 = parts
            self.obj1, self.arg1 = self._search(obj1, self.obj1_search)
            self.obj2, self.arg2 = self._search(obj2, self.obj2_search)

        self.room = caller.location
        self.roomstate = self.room.db.state

    @property
    def focus(self):
        return self.caller.attributes.get("focus", category=self.room.db.tagcategory)

    @focus.setter
    def focus(self, obj):
        self.caller.attributes.add("focus", obj, category=self.room.tagcategory)

    @focus.deleter
    def focus(self):
        self.caller.attributes.remove("focus", category=self.room.tagcategory)


class CmdGiveUp(CmdEvscapeRoom):
    """
    Give up

    Usage:
      give up

    Abandons your attempts at escaping and of ever winning the pie-eating contest.

    """

    key = "give up"
    aliases = ("abort", "chicken out", "quit", "q")

    def func(self):
        from .menu import run_evscaperoom_menu

        nchars = len(self.room.get_all_characters())
        if nchars == 1:
            warning = _QUIT_WARNING_LAST_CHAR.format(roomname=self.room.name)
            warning = _QUIT_WARNING.format(warning=warning)
        else:
            warning = _QUIT_WARNING_CAN_COME_BACK.format(roomname=self.room.name)
            warning = _QUIT_WARNING.format(warning=warning)

        ret = yield (warning)
        if ret.upper() == "QUIT":
            self.msg("|R ... Oh. Okay then. Off you go.|n\n")
            yield (1)

            self.room.log(f"QUIT: {self.caller.key} used the quit command")

            # manually call move hooks
            self.room.msg_room(self.caller, f"|r{self.caller.key} gave up and was whisked away!|n")
            self.room.at_object_leave(self.caller, self.caller.home)
            self.caller.move_to(
                self.caller.home, quiet=True, move_hooks=False, move_type="teleport"
            )

            # back to menu
            run_evscaperoom_menu(self.caller)
        else:
            self.msg("|gYou're staying? That's the spirit!|n")


class CmdLook(CmdEvscapeRoom):
    """
    Look at the room, an object or the currently focused object

    Usage:
      look [obj]

    """

    key = "look"
    aliases = ["l", "ls"]
    obj1_search = None
    obj2_search = None

    def func(self):
        caller = self.caller
        target = self.obj1 or self.obj2 or self.focus or self.room
        # the at_look hook will in turn call return_appearance and
        # pass the 'unfocused' kwarg to it
        txt = caller.at_look(target, unfocused=(target and target != self.focus))
        self.room.msg_char(caller, txt, client_type="look")


class CmdWho(CmdEvscapeRoom, default_cmds.CmdWho):
    """
    List other players in the game.

    Usage:
      who
      who all

    Show who is in the room with you, or (with who all), who is online on the
    server as a whole.

    """

    key = "who"

    obj1_search = False
    obj2_search = False

    def func(self):
        caller = self.caller

        if self.args == "all":
            table = self.style_table("|wName", "|wRoom")
            sessions = SESSION_HANDLER.get_sessions()
            for session in sessions:
                puppet = session.get_puppet()
                if puppet:
                    location = puppet.location
                    locname = location.key if location else "(Outside somewhere)"
                    table.add_row(puppet, locname)
                else:
                    account = session.get_account()
                    table.add_row(account.get_display_name(caller), "(OOC)")

            txt = (
                f"|cPlayers active on this server|n:\n{table}\n"
                "(use 'who' to see only those in your room)"
            )

        else:
            chars = [
                f"{obj.get_display_name(caller)} - {obj.db.desc.strip()}"
                for obj in self.room.get_all_characters()
                if obj != caller
            ]
            chars = "\n".join([f"{caller.key} - {caller.db.desc.strip()} (you)"] + chars)
            txt = f"|cPlayers in this room (room-name '{self.room.name}')|n:\n  {chars}"
        caller.msg(txt)


class CmdSpeak(Command):
    """
    Perform an communication action.

    Usage:
      say <text>
      whisper
      shout

    """

    key = "say"
    aliases = [";", "shout", "whisper"]
    arg_regex = r"\w|\s|$"

    def func(self):
        args = self.args.strip()
        caller = self.caller
        action = self.cmdname
        action = "say" if action == ";" else action
        room = self.caller.location

        if not self.args:
            caller.msg(f"What do you want to {action}?")
            return
        if action == "shout":
            args = f"|c{args.upper()}|n"
        elif action == "whisper":
            args = f"|C({args})|n"
        else:
            args = f"|c{args}|n"

        message = f"~You ~{action}: {args}"

        if hasattr(room, "msg_room"):
            room.msg_room(caller, message)
            room.log(f"{action} by {caller.key}: {args}")


class CmdEmote(Command):
    """
    Perform a free-form emote. Use /me to
    include yourself in the emote and /name
    to include other objects or characters.
    Use "..." to enact speech.

    Usage:
        emote <emote>
        :<emote

    Example:
        emote /me smiles at /peter
        emote /me points to /box and /lever.

    """

    key = "emote"
    aliases = [":", "pose"]
    arg_regex = r"\w|\s|$"

    def you_replace(match):
        return match

    def room_replace(match):
        return match

    def func(self):
        emote = self.args.strip()

        if not emote:
            self.caller.msg('Usage: emote /me points to /door, saying "look over there!"')
            return

        speech_clr = "|c"
        obj_clr = "|y"
        self_clr = "|g"
        player_clr = "|b"
        add_period = not _RE_EMOTE_PROPER_END.search(emote)

        emote = _RE_EMOTE_SPEECH.sub(speech_clr + r"\1\2|n", emote)
        room = self.caller.location

        characters = room.get_all_characters()
        logged = False
        for target in characters:
            txt = []
            self_refer = False
            for part in _RE_EMOTE_NAME.split(emote):
                nameobj = None
                if part.startswith("/"):
                    name = part[1:]
                    if name == "me":
                        nameobj = self.caller
                        self_refer = True
                    else:
                        match = self.caller.search(name, quiet=True)
                        if len(match) == 1:
                            nameobj = match[0]
                if nameobj:
                    if target == nameobj:
                        part = f"{self_clr}{nameobj.get_display_name(target)}|n"
                    elif nameobj in characters:
                        part = f"{player_clr}{nameobj.get_display_name(target)}|n"
                    else:
                        part = f"{obj_clr}{nameobj.get_display_name(target)}|n"
                txt.append(part)
            if not self_refer:
                if target == self.caller:
                    txt = [f"{self_clr}{self.caller.get_display_name(target)}|n "] + txt
                else:
                    txt = [f"{player_clr}{self.caller.get_display_name(target)}|n "] + txt
            txt = "".join(txt).strip() + ("." if add_period else "")
            if not logged and hasattr(self.caller.location, "log"):
                self.caller.location.log(f"emote: {txt}")
                logged = True
            target.msg(txt)


class CmdFocus(CmdEvscapeRoom):
    """
    Focus your attention on a target.

    Usage:
      focus <obj>

    Once focusing on an object, use look to get more information about how it
    looks and what actions is available.

    """

    key = "focus"
    aliases = ["examine", "e", "ex", "unfocus"]

    obj1_search = None

    def func(self):
        if self.obj1:
            old_focus = self.focus
            if hasattr(old_focus, "at_unfocus"):
                old_focus.at_unfocus(self.caller)

            if not hasattr(self.obj1, "at_focus"):
                self.caller.msg("Nothing of interest there.")
                return

            if self.focus != self.obj1:
                self.room.msg_room(
                    self.caller, f"~You ~examine *{self.obj1.key}.", skip_caller=True
                )
            self.focus = self.obj1
            self.obj1.at_focus(self.caller)
        elif not self.focus:
            self.caller.msg("What do you want to focus on?")
        else:
            old_focus = self.focus
            del self.focus
            self.caller.msg(f"You no longer focus on |y{old_focus.key}|n.")


class CmdOptions(CmdEvscapeRoom):
    """
    Start option menu

    Usage:
      options

    """

    key = "options"
    aliases = ["option"]

    def func(self):
        from .menu import run_option_menu

        run_option_menu(self.caller, self.session)


class CmdGet(CmdEvscapeRoom):
    """
    Use focus / examine instead.

    """

    key = "get"
    aliases = ["inventory", "i", "inv", "give"]

    def func(self):
        self.caller.msg("Use |wfocus|n or |wexamine|n for handling objects.")


class CmdRerouter(default_cmds.MuxCommand):
    """
    Interact with an object in focus.

    Usage:
       <action> [arg]

    """

    # reroute commands from the default cmdset to the catch-all
    # focus function where needed. This allows us to override
    # individual default commands without replacing the entire
    # cmdset (we want to keep most of them).

    key = "open"
    aliases = ["@dig", "@open"]

    def func(self):
        # reroute to another command
        from evennia.commands import cmdhandler

        cmdhandler.cmdhandler(
            self.session, self.raw_string, cmdobj=CmdFocusInteraction(), cmdobj_key=self.cmdname
        )


class CmdFocusInteraction(CmdEvscapeRoom):
    """
    Interact with an object in focus.

    Usage:
       <action> [arg]

    This is a special catch-all command which will operate on
    the current focus. It will look for a method
        `focused_object.at_focus_<action>(caller, **kwargs)` and call
    it. This allows objects to just add a new hook to make that
    action apply to it. The obj1, prep, obj2, arg1, arg2 are passed
    as keys into the method.

    """

    # all commands not matching something else goes here.
    key = syscmdkeys.CMD_NOMATCH

    obj1_search = None
    obj2_search = None

    def parse(self):
        """
        We assume this type of command is always on the form `command [arg]`

        """
        self.args = self.args.strip()
        parts = self.args.split(None, 1)
        if not self.args:
            self.action, self.args = "", ""
        elif len(parts) == 1:
            self.action = parts[0]
            self.args = ""
        else:
            self.action, self.args = parts
        self.room = self.caller.location

    def func(self):
        focused = self.focus
        action = self.action

        if focused and hasattr(focused, f"at_focus_{action}"):
            # there is a suitable hook to call!
            getattr(focused, f"at_focus_{action}")(self.caller, args=self.args)
        else:
            self.caller.msg("Hm?")


class CmdStand(CmdEvscapeRoom):
    """
    Stand up from whatever position you had.

    """

    key = "stand"

    def func(self):
        # Positionable objects will set this flag on you.
        pos = self.caller.attributes.get("position", category=self.room.tagcategory)

        if pos:
            # we have a position, clean up.
            obj, position = pos
            self.caller.attributes.remove("position", category=self.room.tagcategory)
            del obj.db.positions[self.caller]
            self.room.msg_room(self.caller, "~You ~are back standing on the floor again.")
        else:
            self.caller.msg("You are already standing.")


class CmdHelp(CmdEvscapeRoom, default_cmds.CmdHelp):
    """
    Get help.

    Usage:
      help <topic> or <command>

    """

    key = "help"
    aliases = ["?"]

    def func(self):
        if self.obj1:
            if hasattr(self.obj1, "get_help"):
                helptxt = self.obj1.get_help(self.caller)
                if not helptxt:
                    helptxt = f"There is no help to be had about {self.obj1.get_display_name(self.caller)}."
            else:
                helptxt = (
                    f"|y{self.obj1.get_display_name(self.caller)}|n is "
                    "likely |rnot|n part of any of the Jester's trickery."
                )
        elif self.arg1:
            # fall back to the normal help command
            super().func()
            return
        else:
            helptxt = _HELP_SUMMARY_TEXT
        self.caller.msg(helptxt.rstrip())


# Debug/help command


class CmdCreateObj(CmdEvscapeRoom):
    """
    Create command, only for Admins during debugging.

    Usage:
      createobj name[:typeclass]

    Here, :typeclass is a class in evscaperoom.commands

    """

    key = "createobj"
    aliases = ["cobj"]
    locks = "cmd:perm(Admin)"

    obj1_search = False
    obj2_search = False

    def func(self):
        caller = self.caller
        args = self.args

        if not args:
            caller.msg("Usage: createobj name[:typeclass]")
            return

        typeclass = "EvscaperoomObject"
        if ":" in args:
            name, typeclass = (part.strip() for part in args.rsplit(":", 1))

        if typeclass.startswith("state_"):
            # a state class
            typeclass = "evscaperoom.states." + typeclass
        else:
            name = args.strip()

        obj = create_evscaperoom_object(typeclass=typeclass, key=name, location=self.room)
        caller.msg(f"Created new object {name} ({obj.typeclass_path}).")


class CmdSetFlag(CmdEvscapeRoom):
    """
    Assign a flag to an object. Admin use only

    Usage:
      flag <obj> with <flagname>

    """

    key = "flag"
    aliases = ["setflag"]
    locks = "cmd:perm(Admin)"

    obj1_search = True
    obj2_search = False

    def func(self):
        if not self.arg2:
            self.caller.msg("Usage: flag <obj> with <flagname>")
            return

        if hasattr(self.obj1, "set_flag"):
            if self.obj1.check_flag(self.arg2):
                self.obj1.unset_flag(self.arg2)
                self.caller.msg(f"|rUnset|n flag '{self.arg2}' on {self.obj1}.")
            else:
                self.obj1.set_flag(self.arg2)
                self.caller.msg(f"|gSet|n flag '{self.arg2}' on {self.obj1}.")
        else:
            self.caller.msg(f"Cannot set flag on {self.obj1}.")


class CmdJumpState(CmdEvscapeRoom):
    """
    Jump to a given state.

    Args:
      jumpstate <statename>

    """

    key = "jumpstate"
    locks = "cmd:perm(Admin)"

    obj1_search = False
    obj2_search = False

    def func(self):
        self.caller.msg(f"Trying to move to state {self.args}")
        self.room.next_state(self.args)


# Helper command to start the Evscaperoom menu


class CmdEvscapeRoomStart(Command):
    """
    Go to the Evscaperoom start menu

    """

    key = "evscaperoom"
    help_category = "EvscapeRoom"

    def func(self):
        # need to import here to break circular import
        from .menu import run_evscaperoom_menu

        run_evscaperoom_menu(self.caller)


# command sets


class CmdSetEvScapeRoom(CmdSet):
    priority = 1

    def at_cmdset_creation(self):
        self.add(CmdHelp())
        self.add(CmdLook())
        self.add(CmdGiveUp())
        self.add(CmdFocus())
        self.add(CmdSpeak())
        self.add(CmdEmote())
        self.add(CmdFocusInteraction())
        self.add(CmdStand())
        self.add(CmdWho())
        self.add(CmdOptions())
        # rerouters
        self.add(CmdGet())
        self.add(CmdRerouter())
        # admin commands
        self.add(CmdCreateObj())
        self.add(CmdSetFlag())
        self.add(CmdJumpState())
