#!/usr/bin/env python

from random import random
from re import split

from commands.command import Command
from evennia.commands.default.general import NumberedTargetCommand
from evennia.commands.default.muxcommand import MuxCommand
from evennia.contrib.rpg.rpsystem import send_emote
from evennia.utils import iter_to_str, logger

from typeclasses.characters import Character
from utils.word_list import routput, paragraph, choices


def speech_effect(speech, verb, target, effects):
    """
    Return speech after applying 'effects'.

    Return a tuple for what a user thinks he said, and what
    others actually here. If no effect, just return speech twice.

    Effects:
      - mute ... can't talk
      - donkied ... target and others hear the value of effect
      - sloshed ... slurred speech

    To administer:

        @set woman/donkied:effect = "Heehaw! ;; Heehaw, heehaw!"
    """
    for effect in effects:
        if effect:
            if effect.db_key == 'donkied':
                msg = choices(effect.value)
                return (msg, msg, "bray")

    return (speech, speech, verb)


class CmdWhisper(MuxCommand):
    """
    Speak privately as your character to another.

    Usage:
      whisper <character> = <message>
      whisper <char1>, <char2> = <message>

    Talk privately to one or more characters in your current location, without
    others in the room being informed.
    """

    key = "whisper"
    priority = 0
    locks = "cmd:all()"
    rhs_split = ("=")

    def func(self):
        """Implement the new 'whisper' command."""
        if not self.args:
            self.caller.msg("What are you whispering?")
            return

        if not self.rhs:
            self.caller.msg("Usage: whisper <character> = <message>")
            return

        targets = [self.caller.search(target) for target in split(r" *, *", self.lhs)]
        full_speech = f"/Me whispers to you, \"{self.rhs}\""
        send_emote(self.caller, targets, full_speech, msg_type="say", anonymous_add=None, quiet=True)

        to_list = [target.get_display_name(self) for target in targets]
        full_speech = f"You whisper to {iter_to_str(to_list, endsep='and')}, \"{self.rhs}\""
        self.caller.msg(full_speech, from_obj=self.caller)


class CmdSay(MuxCommand):
    """
    Say something to the characters in the same area.

    For instance:

          |gsay Good evening!|n

    However, you can use an alias and simply type:

          |g"Good evening!

    Or:

          |g'Good evening!

    Creatures and objects in this game can sometimes respond to what you say.

    Usage:
      say phrase
      say/to char1, [char2 ...] = phrase
      say[/switches] phrase

    Where switches can be any of the following:

      - exclaim : To replace 'says' with 'exclaims'
      - yell : To replace 'says' with 'yells'
      - scream : To replace 'says' with 'screams'
      - ask : To replace 'says' with 'asks'
      - to : Directs phrase to one or more characters
             in the same area. Note others can still hear
             the statement (see the 'whisper' command).
      - adverb : Any adverb-like word that ends in '-ly'
             is added to the say command, for instance:

          say/quietly Hi there.

        Shows as:

          You quietly say, "Hi there."
    """

    key = "say"
    aliases = ["says", "speak", "shout", "yell", "exclaim", "scream", "ask",
               "reply", "respond", "\"", "'"]
    priority = 0
    locks = "cmd:all()"
    rhs_split = ("=")
    arg_regex = None

    def func(self):
        """Implement the new 'say' command with switches."""
        speaker = self.caller

        if not self.args:
            speaker.msg("Say what?")
            return

        if 'to' in self.switches and not self.rhs:
            speaker.msg(paragraph("""
            When attempting to say something to one or more
            characters, use the '=' character to identify what you
            want to say.
            For instance, |gsay/to elf = Hi there.|n """))
            return

        if self.rhs:
            speech = self.rhs
        else:
            speech = self.args

        # If speech is empty, stop here
        if not speaker.at_pre_say(speech):
            return

        adverb = ''
        for switch in self.switches:
            if switch.endswith('ly'):
                adverb = switch + ' '

        if 'scream' in self.switches or self.cmdstring == 'scream':
            verb = 'scream'
        elif 'shout' in self.switches or self.cmdstring == 'shout':
            verb = 'shout'
        elif 'respond' in self.switches or self.cmdstring == 'respond':
            verb = 'respond'
        elif 'reply' in self.switches or self.cmdstring == 'reply':
            verb = 'reply'
        elif 'yell' in self.switches or self.cmdstring == 'yell':
            verb = 'yell'
        elif 'exclaim' in self.switches or self.cmdstring == 'exclaim' or speech.endswith('!'):
            verb = 'exclaim'
        elif 'ask' in self.switches or self.cmdstring == 'ask' or speech.endswith('?'):
            verb = 'ask'
        else:
            verb = "say"

        for_me, for_others, verb = \
            speech_effect(speech, verb,
                          speaker,
                          speaker.attributes.get(category="effect",
                                                 return_obj=True,
                                                 return_list=True))

        to_who = to_whom = ''

        if 'to' not in self.switches:
            for char in speaker.location.contents:
                if hasattr(char, 'other_say') and callable(char.other_say):
                    char.other_say(speaker, for_others)
        else:
            # Send the message to 'puppets' that have the special 'other_sayto'
            # method (if they are the object of the message):
            targets = split(r" *, *", self.lhs)
            to_chars = [speaker.search(c, quiet=True) for c in targets]
            to_chars = [c[0] for c in to_chars if len(c) > 0]

            if to_chars == []:
                speaker.msg(f"No match for {', '.join(targets)}")
                return

            for char in to_chars:
                if hasattr(char, 'other_sayto') and callable(char.other_sayto):
                    logger.info(f"Found {char.key}: {for_others}")
                    char.other_sayto(speaker, for_others)

            who = iter_to_str([c.get_display_name(speaker)
                               if isinstance(c, Character) else c
                               for c in to_chars])
            whom = iter_to_str([f"/{c}" for c in targets])

            if verb == "ask":
                to_who = " " + who
                to_whom = " " + whom
            else:
                to_who = " to " + who
                to_whom = " to " + whom

        # The `send_emote` is _global_, so if we want to say to 'Bob',
        # 'You say ...', we have to both send him a message with the
        # 'You' as well as everyone else with the 'send_emote'.
        full_speech = f"You {adverb}{verb}{to_who}, \"{for_me}\""
        speaker.msg(full_speech, from_obj=speaker)

        # English is weird...
        if verb == 'reply':
            verb = 'replie'

        targets = [item for item in speaker.location.contents
                   if item != speaker]
        full_speech = f"/me {adverb}{verb}s{to_whom}, \"{for_others}\""
        send_emote(speaker, targets, full_speech, msg_type="say",
                   anonymous_add=None)


class CmdThink(Command):
    """Think a thought out loud.

    Usage:
      think <message>

    Similar to the 'say' or 'pose' commands, this communicates an
    inner monologue to other players on the 'public' channel.
    """

    key = "think"
    aliases = ["thinks", "("]
    arg_regex = None

    def func(self):
        """Implement the think out loud command."""
        if not self.args:
            self.caller.msg("What do you want to think out loud?")
        else:
            thought = self.args.strip()
            if (self.caller.db.thinking_count or 0) < 3 or random() < 0.4:
                msg = routput(
                    f"<< thinks ^ wonders >> << out loud ^ aloud >> "
                    f"... o O ( {thought} )"
                )
            else:
                msg = f". o O ( {thought} )"
            self.caller.db.thinking_count = (self.caller.db.thinking_count or 0) + 1

            self.caller.execute_cmd(f"pub :{msg}")


class CmdRead(Command):
    """
    Return the inside contents of a book or other readable object.

    Usage:

       read <target>

    To add something to read on target, use the @set command:

      @set <target>/inside = 'This is the text to read.'
    """

    key = "read"

    def find_readable(self, searcher, readable_str):
        """Search the room for a readable item."""
        targets = searcher.search(readable_str, quiet=True)
        if not targets:
            searcher.msg(f"You don't see {readable_str}.")
            return None

        label_targets = [t for t in targets if t.db.inside]

        if len(label_targets) == 1:
            return label_targets[0]

        if len(label_targets) == 0:
            searcher.msg(f"You can't find anything readable on {readable_str}.")
        else:
            searcher.msg(f"Too many things match, '{readable_str}'. "
                         "Can you narrow it down with a title?")

    def func(self):
        """Return the 'inside' attribute."""
        target_str = self.args.strip()
        if target_str == "":
            self.caller.msg("Usage: |gread <object>|n")
            return

        book = self.find_readable(self.caller, target_str)
        if book:
            self.caller.msg(book.db.inside)


class CmdTake(Command, NumberedTargetCommand):
    """
    Take an object from another character or NPC.

    Usage:
      take <thing> from <character>

    Note that only some things can be stolen.
    For instance, the brass ring from the door knocker.
    """

    key = "take"
    aliases = ["steal"]
    rhs_split = ("=", " from ")

    def func(self):
        """
        Implement the take command.

        Since this command is designed to work on the object, we
        operate only on self.obj.
        """
        if not self.args:
            self.caller.msg("What do you want to take?")
        elif not self.rhs:
            self.caller.msg(f"You want to take {self.lhs}, but from whom?")
        else:
            self.caller.do_take(self.lhs, self.rhs)


class CmdDrink(Command):
    """
    Drink a beverage in your inventory.

    Usage:

      drink [ container ]

    If you are holding a teacup, or cocktail, and it is not empty,
    you may drink.

    This doesn't tell others of this particular activity.
    """

    key = "drink"
    aliases = ["sip", "quaff"]

    def not_empty(self, item):
        """Return true is the cup has some drink left in it."""
        return (item.db.amount or 0) > 0

    def drink_item(self, name):
        """Find item in inventory, name, and call 'do_drink' on it."""
        notfound = f"You don't have {name} in your inventory."
        item = self.caller.search(name, location=self.caller,
                                  nofound_string=notfound)
        if item:
            if item.has_method('do_drink'):
                item.do_drink(self.caller)
            else:
                self.caller.msg(f"The {item.name} is not drinkable.")

    def drink_anything(self):
        """Drink anything in your inventory, but only if you have one thing."""
        containers = [item for item in self.caller.contents
                      if item.has_method('do_drink') and self.not_empty(item)]
        if len(containers) == 1:
            containers[0].do_drink(self.caller)
        elif len(containers) > 1:
            self.caller.msg("You have too many things you can drink. "
                            "Which one do you want?")
        else:
            self.caller.msg("You have nothing to drink.")

    def func(self):
        """
        If given the name of something to drink, find and drink it.

        Otherwise, drink the first item you find in your inventory.
        """
        goal = self.args.strip()
        if goal and goal != "":
            self.drink_item(goal)
        else:
            self.drink_anything()


class CmdEat(Command):
    """
    Eat something edible in your inventory.

    Usage:

      eat [ food-item ]

    This doesn't tell others of this particular activity.
    """

    key = "eat"
    aliases = ["consume", "bite"]

    def not_gone(self, item):
        """Return true is the cup has some eat left in it."""
        return (item.db.amount or 0) > 0

    def eat_item(self, name):
        """Find item in inventory, name, and call 'do_eat' on it."""
        notfound = f"You don't have {name} in your inventory."
        item = self.caller.search(name, location=self.caller,
                                  nofound_string=notfound)
        if item:
            if item.has_method('do_eat'):
                item.do_eat(self.caller)
            else:
                self.caller.msg(f"The {item.name} is not edible.")

    def eat_anything(self):
        """Eat something in your inventory, but only if you have one thing."""
        items = [item for item in self.caller.contents
                 if item.has_method('do_eat') and self.not_gone(item)]
        if len(items) == 1:
            items[0].do_eat(self.caller)
        elif len(items) > 1:
            self.caller.msg("You have too many things to eat. "
                            "Which one do you want?")
        else:
            self.caller.msg("You have nothing to eat.")

    def func(self):
        """
        If given the name of something to eat, find and eat it.

        Otherwise, eat the first item you find in your inventory.
        """
        goal = self.args.strip()
        if goal and goal != "":
            self.eat_item(goal)
        else:
            self.eat_anything()
