"""
textgame.parser
=====================
This module's main class is :class:`textgame.parser.Parser`. The parser can take
user input, call a function that's associated to the input and return to the user a
message describing what happened.
Use ``actionmap`` and ``legal_verbs`` to define how verbs should be mapped to functions, eg:
.. code-block:: python
   parser.actionmap.update({
    "scream": player.scream
   })
   parser.legal_verbs.update({
    "scream": "scream",
    "shout": "scream"
   })
You can use ``legal_nouns`` to define synonyms for nouns.
A parser is the only thing needed to during the main loop of a game:
.. code-block:: python
   parser = textgame.parser.Parser(player)
   while player.status["alive"]:
       response = parser.understand( input("> ") )
       print(response)
This module also provides :class:`textgame.parser.EnterYesNoLoop`. If a function
called by the parser returns an ``EnterYesNoLoop`` instead of a string, the parser falls
into a mode where it only allows 'yes' and 'no' as an answer. An object of type
``EnterYesNoLoop`` also provides strings/functions to print/call for each case.
Example: a player method that saves the user from drinking poison
.. code-block:: python
   @action_method
   def drink(self, noun):
       if noun == "poison":
           def actually_do_it():
               self.status["alive"] = False
               return "You drink the poison and die."
           return textgame.parser.EnterYesNoLoop(
                question = "Do you really want to drink poison?",
                yes = actually_do_it,
                no = "You stay alive")
       else:
           # ...
"""
from collections import namedtuple
import logging
logger = logging.getLogger("textgame.parser")
logger.addHandler(logging.NullHandler())
from textgame.globals import INFO
[docs]class EnterYesNoLoop:
    """
    :param question: a yes/no question
    :type question: string
    :param yes: string to return or a function with signature ``f() -> str`` or ``f() -> EnterYesNoLoop`` that should get called if player answeres 'yes' to the question
    :param no: same as yes
    """
    def __init__(self, question, yes, no):
        self.question = question
        self._yes = yes
        self._no = no
[docs]    def yes(self):
        """
        if yes is callable, return its result, else return it
        """
        if callable(self._yes):
            return self._yes()
        return self._yes 
[docs]    def no(self):
        """
        if no is callable, return its result, else return it
        """
        if callable(self._no):
            return self._no()
        return self._no  
[docs]class Parser:
    """
    :param player: :class:`textgame.player.Player` object
    """
    def __init__(self, player):
        self.player = player
        self.in_yesno = False    # are we inside a yes/no conversation?
        # yesno_backup must be a function that takes a bool and returns
        # user output. it will be executed if yes/no conversation ends with yes
        self.yesno_backup = None
        self.legal_verbs = {
            "": "continue",    # dont do anything on empty input
            "attack": "attack",
            "back": "back",
            "close": "close",
            "d": "down",
            "down": "down",
            "drop": "drop",
            "e": "east",
            "east": "east",
            "enter": "go",
            "go": "go",
            "grab": "take",
            "hear": "listen",
            "hint": "hint",
            "inventory": "inventory",
            "kill": "attack",
            "listen": "listen",
            "lock": "close",
            "look": "look",
            "n": "north",
            "north": "north",
            "open": "open",
            "s": "south",
            "score": "score",
            "south": "south",
            "take": "take",
            "u": "up",
            "up": "up",
            "w": "west",
            "walk": "go",
            "west": "west",
        }
        # this may be used to define synonyms
        self.legal_nouns = {
            "d": "down",
            "e": "east",
            "n": "north",
            "s": "south",
            "u": "up",
            "w": "west",
        }
        # the lambdas are there because the values in this dict must be
        # callable with exactly one argument
        self.actionmap = {
            "attack": player.attack,
            "back": lambda x: player.go("back"),
            "continue": lambda x: "",
            "down": lambda x: player.go("down"),
            "drop": player.drop,
            "east": lambda x: player.go("east"),
            "go": player.go,
            "hint": player.ask_hint,
            "inventory": player.list_inventory,
            "listen": player.listen,
            "look": player.look,
            "close": player.close,
            "north": lambda x: player.go("north"),
            "open": player.open,
            "score": player.show_score,
            "south": lambda x: player.go("south"),
            "take": player.take,
            "up": lambda x: player.go("up"),
            "west": lambda x: player.go("west"),
        }
        self.check()
[docs]    def lookup_verb(self, verb):
        return self.legal_verbs.get(verb) 
[docs]    def lookup_noun(self, noun):
        return self.legal_nouns.get(noun) 
[docs]    def check(self):
        """
        check if every verb in self.legal_verbs has a function mapped to.
        if not, the game will crash on the input of this verb
        logs the error
        """
        for verb in set(self.legal_verbs.values()):
            if verb not in self.actionmap:
                logger.error("{} is a legal verb but has no definition"
                    "in actionmap".format(verb)) 
[docs]    def check_result(self, result):
        """
        checks if result is EnterYesNoLoop or str, if it's EnterYesNoLoop,
        return the question and fall back to yes/no mode
        """
        if type(result) is str:
            return result
        else:
            # assume that result is of type enteryesnoloop
            self.in_yesno = True
            self.yesno_backup = result
            return result.question 
[docs]    def do(self, verb, noun):
        """
        call function associated with verb with noun as argument
        """
        return self.actionmap[verb](noun) 
    def _split_input(self, input):
        """
        take input and return verb and noun
        """
        args = input.split()
        if len(args) > 2:
            # this gets catched in Parser.understand
            raise ValueError()
        elif len(args) == 2:
            verb, noun = args
        elif len(args) == 1:
            verb = args[0]
            noun = ""
        else:
            verb = ""
            noun = ""
        return verb, noun
[docs]    def understand(self, input):
        """
        based on the input, perform player method and return its output
        the return value is what can be printed to the user
        """
        try:
            verb, noun = self._split_input(input)
        except ValueError:
            return INFO.TOO_MANY_ARGUMENTS
        # if a yes/no conversation is going on, only allow yes/no as answers
        if self.in_yesno:
            if verb != "yes" and verb != "no":
                return INFO.YES_NO
            elif verb == "yes":
                self.in_yesno = False
                # return the yes case
                result = self.yesno_backup.yes()
                return self.check_result(result)
            else:
                self.in_yesno = False
                # return the no case
                result = self.yesno_backup.no()
                return self.check_result(result)
        commandverb = self.lookup_verb(verb)
        commandnoun = self.lookup_noun(noun)
        # if noun is illegal, reset to it's original value and feed it to
        # the actionmethods. More creative output if erronous input :)
        if not commandnoun:
            commandnoun = noun
        logger.debug("I understood: verb={} noun={}".format(repr(commandverb), repr(commandnoun)))
        # illegal nouns are okay but illegal verbs are not
        if not commandverb:
            return INFO.NOT_UNDERSTOOD
        # perform the associated method
        result = self.do(commandverb, commandnoun)
        return self.check_result(result)