#!/usr/bin/env python

from random import choice, random, randint
from enum import Enum

from evennia import create_script
from evennia.utils import logger, delay

from commands.misc import CmdSetPuddle, CmdSetStick, CmdSetKnock, CmdSetSmoke
from commands.wizards import CmdSetWand
from utils.word_list import routput, choices
from .scripts import KnockScript
from .objects import Object

import re


class Trinket(Object):
    """
    For instance:

    """
    msgs = [
        "You see a crystal ball. When the swirling vortex stops, It reads, '<<Why not? ^ Don't even think about it ^ Maybe, maybe not ^ Sure, just don't tell>>'.",
        "You find a deck of Tarot cards, but the card of Death has been replaced by a worker in an office cubical that reads, 'Mostly Death'.",
        "You see a statue of a <<black ^ red ^ yellow>> dragon in the shape of a question mark. You have to ask yourself, Why?",
        "A Venus fly trap in a shell surrounded by chubby cherubs.",
        "You see a statue of a large rat. Did it just blink? Wow, after it ran away you think that was a rat of unusual size.",
        "An ancient Colombian grave owl holding a sign that reads, 'Cannibals don't eat clowns? They taste funny.'",
        "You find a dirty plate with a pizza crust. This is probably not a trinket as much as just sloppy housecleaning.",
        "You find a vial of colorless, odorless, tasteless Iocane powder. This is among the deadlier poisons known to man, so you should probably not touch it.",
        "A troll doll with an outie belly button. Everyone knows that trolls are not placental mammals, but reproduce through a complex series of... wait, you haven't had this talk with your parents, yet?",
        "A small statue of a flexing giant. When you push its belly, it says, 'Its not my fault I'm the biggest and the strongest. I don't even exercise.'",
        "You see a license plate from the State of Utah that reads HEATHEN, and think, what is the significance of hens in heat? Oh, and you also think, what is a license plate doing in a fantasy game about escaping the clamors of last stage capitalism?",
        "You find a brilliant <<blue ^ green ^ iridescent>> scarf around a book, but as soon as you remove it, the book tries to bite you. After it chases you around the room, you double back, jump on it, breaking its spine (which is lets out a sad yelp), and wrap the scarf back around it. Maybe you've seen enough stuff on these shelves for a while.",
    ]

    def at_object_creation(self):
        """
        Set the initial number position for cycling through them.
        """
        self.db.last_trinket_num = -1

    def return_appearance(self, looker, **kwargs):
        """
        Return a trinket by cycling through the list.
        """
        self.db.last_trinket_num = self.db.last_trinket_num + 1

        # Once they have seen the crystal ball, they can now "see"
        # them, and probably pick one up, if they are around:
        if self.db.last_trinket_num == 0:
            looker.tags.add("hidden_ball")

        # Seen all the trinkets? Oh boy, well, let's loop:
        if self.db.last_trinket_num >= len(self.msgs):
            self.db.last_trinket_num = 0

        return routput(self.msgs[self.db.last_trinket_num])


class CrystalBall(Object):
    def return_appearance(self, looker, **kwargs):
        """Return a different result each time it is looked."""
        return routput(self.db.desc) + " |w" + choice([
            "Definitely yes",
            "It is certain",
            "It is decidedly so",
            "Without a doubt",
            "You may rely on it",
            "As I see it, yes",
            "Most likely",
            "Outlook good",
            "Yes",
            "Signs point to yes",

            "Reply hazy, try again",
            "Ask again later",
            "Better not tell you now",
            "Cannot predict now",
            "Concentrate and ask again",

            "Nope",
            "Don’t count on it",
            "My reply is no",
            "My sources say no",
            "Outlook not so good",
            "Very doubtful",
        ])

class Ring(Object):
    def move_to(self, destination, **kwargs):
        """
        The ring should only go to the door knocker...
        """
        if destination.is_typeclass("typeclasses.things.Knocker") or \
           destination.is_typeclass("typeclasses.characters.Character"):
            super().move_to(destination)
            return True
        return False


class Pipe(Object):
    """Simple abstraction for the following actions.

    Note that each message has two versions, one for the smoker (you)
    and one for everyone else in the room.

    @set pipe/light_msg = "You pull out, pack and light a pipe."
    @set pipe/light_msg_other = "{0} packs |p pipe and lights it."

    Where the |p is a possessive gender, if set, e.g. his or her.

    The random messages has available substitutions based on if the
    message is for the smoker or for the audience in the room.
    Specifically:

    - {0} :: either "you" or your name
    - {1} :: either "your" or your name with an apostrophe 's.
    - {2} :: blank (for you) or "s" for everyone else, e.g. "blow{2}"

    For instance:

    @set pipe/random_msgs = "{0} blow{1} a <<large ^ small ^ >> smoke-ring followed by another that flies through the first. ;; {1} smoke collesce to form a <<dragon ^ large woodland beast ^ beholder ^ bugbear>> ... or

    """
    def at_object_creation(self):
        self.cmdset.add_default(CmdSetSmoke)

    def do_light(self, lighter):
        msg = choices(self.db.light_msg or "$You() $conj(pack) and $conj(light) $pron(your) pipe.")
        lighter.announce_action(msg)

    def do_smoke(self, smoker):
        msg = choices(self.db.smoke_msg or "$You() << $conj(lean) back and ^ >> << $conj(puff) ^ $conj(smoke) >> $pron(your) pipe.")
        smoker.announce_action(msg)

    def do_ring(self, smoker, details=None):
        self.db._last_option = 'ring'
        self.db._last_detail = details

        details = details or ""
        msg = choices(f"$You() $conj(blow) a {details} << smoke ring ^ ring of smoke >>.")
        smoker.announce_action(msg)
        if randint(1, 10) < 5:
            delay(randint(5, 15), self.do_ring_dissipate)

    def do_ring_dissipate(self):
        msg = choices("""
        The smoke ring << flies around before it ^ shoots across the room before it ^ >> << eventually ^ finally >> << dissipates ^ disperses >>. ;;
        The ring hovers, spinning in place, before << dissipating ^ dispersing >>. ;;
        The smoke ring changes << colors ^ to purple ^ to blue ^ to pink>> before << dissipating ^ dispersing >>.
        """)
        self.location.location.msg_contents(msg)

    def do_dragon(self, smoker, details=None):
        self.db._last_option = 'dragon'
        self.db._last_detail = details

        details = details or ""
        msg = choices(f"The smoke from $pron(your) pipe << coalesces ^ coheres >> << to form ^ into the shape of ^ and resembles ^ to look like >> a {details} dragon.")
        smoker.announce_action(msg)

        if randint(1, 10) < 8:
            delay(randint(5, 15), self.do_dragon_dissipate)

    def do_dragon_dissipate(self):
        msg = choices("""
        The smoke dragon << flies around ^ soars across the room ^ bellows >> before it << eventually ^ finally >> << dissipates ^ disperses >>. ;;
        The smoke dragon changes << colors ^ to purple ^ to blue ^ to pink>> before << dissipating ^ dispersing >>.
        """)
        self.location.location.msg_contents(msg)


class Wood(Object):
    "An object to burn."
    def at_object_creation(self):
        self.db.singular = "a log"
        self.db.plural = "some logs"
        self.db.desc = routput(choice([
            "Its log, its log, it's big, it's heavy, it's wood.",
            "Some <<logs ^ wood>> for the fireplace.",
        ]))


class Stick(Object):
    "An object to throw."
    def at_object_creation(self):
        self.cmdset.add_default(CmdSetStick)

    def do_throw(self, thrower):
        beast = thrower.location.search('beast')
        if beast and beast.db.is_awake:
            beast.thrown_stick(thrower)
        elif thrower.location.is_typeclass("typeclasses.rooms_weather.TimeWeatherRoom"):
            thrower.db.thrown_times = (thrower.db.thrown_times or 0) + 1
            if thrower.db.thrown_times == 1:
                thrower.msg("The stick flies through the air, and just as you thought, it comes back to you!")
            else:
                thrower.msg(choices([
                    "Yer a wizard, {0}!",
                    "<<Did you see that? ^ >> It clipped the leaf on that tree before returning.",
                    "This is a <<fun ^ pleasant ^ nice>> <<past-time ^ game>>.",
                    "Maybe we should get a pet <<or beast ^ beastie ^ >> in here who would love to play.",
                ], thrower.name.capitalize()))
                thrower.announce_action("$You() $conj(throw) $pron(your) stick.",
                                        exclude=thrower)
        else:
            thrower.msg("I think you should be outside or a place with more room before you throw that stick around.")


class Wand(Stick):
    def at_object_creation(self):
        self.cmdset.add_default(CmdSetWand)


class Puddle(Object):
    def at_object_creation(self):
        self.cmdset.add_default(CmdSetPuddle)

    def do_jump(self, player):
        player.db.jumped_times = (player.db.jumped_times or 0) + 1
        if player.db.jumped_times == 1:
            player.msg("You jump in the puddle! "
                       "This is great fun. You feel childish.")
        else:
            player.msg(routput(choice([
                "You don't care <<how muddy ^ who notices>>, you jump in again!",
                "Mud? Whatever. You <<splash ^ jump>> in <<^ again>>.",
                "You splash around in the puddle.",
                "You jump in the puddle again.",
                "This time you dance a little as you kick up your heels.",
            ]) + " You feel " + choice([
                "<<so much ^ >> better.",
                "carefree.",
                "child-like and free.",
                "irresponsible.",
                "happy.",
                "great.",
            ])))
        player.announce_action("$You() << $conj(jump) ^ $conj(splash) ^ $conj(play) >> in the puddle.",
                               exclude=player)


class Knocker_Convo(Enum):
    "What topics have we covered?"
    ANY = 0
    GREETED = 1
    DOOR = 2
    LOCK = 3
    PASS = 4
    HINT = 5
    RIDDLE = 6
    ANSWERED = 7

class Knocker(Object):
    """
    Knocker

    The Object is the class for gatekeepers between rooms.
    Special object that listens to what is said in the room, and
    attempts to a _real_ NPC.
    """
    muffled_responses = [
        "Mmmuufffmm",
        "Mmmf?",
        "Mummffmmph",
        "Umfmuummmfff",
        "Umf! Umfmmmf mmm mmmuufffmm mff mmuuummph.",
    ]

    yes_msg = r"\byes|yeah|yah|sure|please\b"
    no_msg = r"\bno|nope\b"
    question_msg = r"\?$"
    greet_msg = r"\bhello|\bgreet|\bhey\b"

    # Each state of conversation is listed:
    close_responses = [
        "Oooo...good guess, but we are looking for something more specific.",
    ]
    answered_responses = [
        "Alright, I'll open the door for yah.",
        "I heard my favorite, let's get this door unlocked.",
    ]

    # Note that the responses are ordered from most specific to least.
    knocker_responses = [
        "[Sorry.|What was that?|Did you say something?] I'm hard of hearing on account of the brass ears.",
        "Yes, I suppose I'm this amazing puzzle you get to in Chapter Three. Wait, does that mean I'm just an NPC?",
        "Who me? I thought you were talking to the goblin in the bushes.",
        "Why yes, I am hard of hearing.",
    ]
    password_responses = [
        "Of course this door is protected by a super complicated encrypted password.",
        "If I tell you, it wouldn't be a secret now.",
        "The password? You just have to guess...unless you want a hint?",
        "Well, I suppose I could give you a |mhint|n.",
    ]
    hint_responses = [
        "A hint does sound fair. <<Should I|I should>> come up with a |mriddle|n<<, huh ^ >>?"
    ]
    riddle_responses = [
        """
        Aged in barrels, smooth and neat,
        In a glass by the fire, I'm a treat.
        """,
        """
        A scent of oak, a whiff of grain,
        A drop of expertise from the cask remains.
        """,
        """
        In oaken halls, I sleep and bide,
        till I'm called to warm your insides...
        hrm, that's a little vague, but kinda nice.
        """,
        """
        Alright, alright, the riddle should be clever.

        It should refer to its golden hue, and I should make it obvious that it isn't gold, and adding a reference to quest of the Argonauts would be a complete red herring, and quite a mean thing to do, so I shouldn't add that, but what about barrels? Yeah, need to include barrels, but not the way Bilbo road barrels, for that was definitely intended as a misleading riddle for Smaug, but of course, a smart dragon would always figure such things out, wait, where was I?"""
    ]

    locked_responses = [
                "Yes, I'm familiar with the door and the fact that it is locked.",
                "This locked door is to protect the theft of Dabbler's scones.",
            ]
    all_responses = [
        # The password must be the first entry here, as we "act" on it:
        [r"whiske?y", Knocker_Convo.ANY, answered_responses, Knocker_Convo.ANSWERED],
        [r"scotch|bourbon", Knocker_Convo.ANY, [
            "A little more specific than needed, but that will do."],
         Knocker_Convo.ANSWERED],

        # A _sequence_ of responses, from what is this password business, to give me a hint, to give me the riddle ...
        [question_msg, Knocker_Convo.RIDDLE, [
            "Nope. Would <<ya ^ you>> like another riddle?",
            "You <<really ^ >> think that is the answer?",
            "Personally, <<I think ^>> that riddle could not <<be ^ have been>> more clear."
        ], None],
        [yes_msg, Knocker_Convo.HINT, riddle_responses, Knocker_Convo.RIDDLE],
        ["riddle", Knocker_Convo.ANY, riddle_responses, Knocker_Convo.RIDDLE],
        ["another", Knocker_Convo.RIDDLE, riddle_responses, Knocker_Convo.RIDDLE],

        ["hint", Knocker_Convo.ANY, hint_responses, Knocker_Convo.HINT],
        ["\btell\b", Knocker_Convo.PASS, hint_responses, Knocker_Convo.HINT],
        [yes_msg, Knocker_Convo.PASS, hint_responses, Knocker_Convo.HINT],
        [question_msg, Knocker_Convo.PASS, hint_responses, Knocker_Convo.HINT],

        [yes_msg, Knocker_Convo.DOOR, password_responses, Knocker_Convo.PASS],
        ["password", Knocker_Convo.ANY, password_responses, Knocker_Convo.PASS],
        ["get in", Knocker_Convo.LOCK, password_responses, Knocker_Convo.PASS],

        ["get in", Knocker_Convo.DOOR, [
            "Well, you see. The door is locked."
        ], Knocker_Convo.LOCK],
        [question_msg, Knocker_Convo.DOOR, locked_responses, Knocker_Convo.LOCK],
        [r"\block", Knocker_Convo.ANY, locked_responses, Knocker_Convo.LOCK],

        [r"\bdoor\b", Knocker_Convo.ANY, [
            "What door? I don't see a door. Ha!",
            "That's right, I am the clever puzzle hanging on a door.",
            "I'm hanging on a door? Really? Let's see, can I roll my eyes?",
            "Just to let you know, the door is locked.",
            """
            I shouldn't be telling you this, but I like the cut of your <<suit ^ cloak ^ jib>>.
            So, you see, if you speak the |mpassword|n, wait, I've said too much.
            """
        ], Knocker_Convo.DOOR],

        [greet_msg, Knocker_Convo.GREETED, [
            "Didn't we just exchanged pleasantries?",
            "Just so you know, I'm not much into small talk."
        ], None],
        [greet_msg, Knocker_Convo.ANY, [
            "How's it going?",
            "How's it?",
            "'Sup.",
            "How are you?",
        ], Knocker_Convo.GREETED],

        [r"knocker|goblin", Knocker_Convo.ANY, knocker_responses, None],

        [r"\bass\b", Knocker_Convo.ANY, [
            "Why yes, I am made of brass."
        ], None],

        [r"thanks|thank you", Knocker_Convo.ANY, [
            "You're <<very ^ >> welcome.",
            "Glad to be of service."
        ], None],

        [yes_msg, Knocker_Convo.ANY, [
            "Really? You agree?",
            "Excellent.",
        ], None],
        [no_msg, Knocker_Convo.ANY, [
            "Well, it's true. Just ask the <<raven ^ trees ^ gnome>>.",
        ], None],
        [question_msg, Knocker_Convo.ANY, [
            "What about <<me ^ it ^ 'im>>?",
            "I dunno...",
        ], None],
    ]

    after_unlocked_responses = [
        "We could <<feign ^ pretend ^ play make-believe>> and <<carry on ^ continue ^ persist>> this <<conversation ^ charade>>, but we both know that |wyou|n know the <<password ^ secret ^ secret word ^ magic>>, and can go through the |gdoor|n now.",
        "<<Sure ^ Why not ^ Why yes>>, let's <<feign ^ pretend ^ play make-believe that>> you don't know the <<password ^ secret ^ secret word ^ magic>>, and <<carry on ^ continue ^ persist>> this conversation.",
    ]

    cant_hear_responses = [
        "<<Sorry. ^ What was that? ^ Did you say something?>> I'm hard of hearing on account of the brass ears.",
        "Brass ears. Yeah, not the best at hearing.",
        "Yeah, These brass ears don't hear much except for the |m<<secret ^ magic ^ >> password|n.",
    ]

    unknown_responses = [
        "Are you talking to me or the goblin <<in the bushes ^ up the tree ^ behind the rock>>?",
        "Knock, knock.",
        "What do you mean?",
        "Of course I like <<hard candy ^ squirrels ^ apples>>. Who <<doesn't ^ wouldn't ^ do you know that doesn't>>?",
        "No thank you, I can't eat <<apples ^ kumquats ^ figs>>, what with the brass teeth and a lack of guts.",
        "Tea? While that would be <<nice ^ sweet>> of you, I can't really hold a cup.",
        "I'm fine, thanks. How are you?",
        "I'll say, <<we have had ^ that is>> a spell of weather.",
    ]

    def at_object_creation(self):
        self.cmdset.add_default(CmdSetKnock)

    def do_knock(self, knocker):
        if self.has("ring"):
            knock_script = create_script(key="knocking",
                                         typeclass=KnockScript,
                                         interval=10,  # seconds <=0 means off
                                         start_delay=True,  # wait interval before first call
                                         autostart=True,
                                         # This is the _character_
                                         # that does the knocking, not
                                         # the door knocker:
                                         attributes=[("knocker", knocker),
                                                     # Waker is the door knocker
                                                     ("waker", self),
                                                     ("room", self.db.room_to_msg)
                                                     ])
            knocker.announce_action("$You() $conj(grab) the ring and $conj(knock) << firmly ^ loudly ^ aggressively >> on the door.")
        else:
            knocker.msg("This door knocker is defective, as it doesn't have a ring to...er, do the knockin'.")

    def knocked_timed_out(self):
        if self.has("brass ring"):
            self.at_say(choice(self.muffled_responses))
        else:
            self.at_say("Doesn't appear that anyone is home.")

    def at_heard_say(self, message, talker):
        """
        A simple listener and response. This makes it easy to change for
        subclasses of NPCs reacting differently to says.
        """

        curr_state = Knocker_Convo(talker.db.knocker_conversation_state or 0)
        # message will be on the form `<Person> says, "say_text"`
        # we want to get only say_text without the quotes and any spaces
        if message.count('"') == 0:
            return

        message = message.split('"')[1].strip()

        # Let's see if a keyword gives a good response:
        for [regex, convo_state, responses, new_state] in self.all_responses:
            full_regex = r".*" + regex + r".*"

            if re.match(full_regex, message, re.IGNORECASE) \
               and curr_state.value >= convo_state.value:
                # logger.info(f"Spoke: {message}, matched: {full_regex} and {curr_state} >= {convo_state}")

                # If we have the ring in our mouth, we are muffled:
                if self.has("brass ring"):
                    return choice(self.muffled_responses)
                else:
                    if new_state and new_state.value > curr_state.value:
                        talker.db.knocker_conversation_state = new_state

                    # The first match is the password, so we set a tag
                    # on the character, so that they can go through
                    # the door:
                    if talker.db.knocker_conversation_state == Knocker_Convo.ANSWERED:
                        talker.tags.add("open_red_door", category="mp")

                    return routput(choice(responses))



        # We can not do a random response. Of course, we are
        # pretending we are hard of hearing, so we don't spam the room
        # _every_ time:
        if random() * 100 < 45:
            if self.has("brass ring"):
                return choice(self.muffled_responses)

            # If a keyword was not spoken, we want to emphasize that
            # we are hard of hearing, and mention that every other
            # time, so we store in the database a setting to keep
            # track and alternate the responses:
            elif self.db.hard:
                self.db.hard = False
                if talker.tags.get(key="open_red_door", category="mp"):
                    return routput(choice(self.after_unlocked_responses))
                else:
                    return routput(choice(self.unknown_responses))
            else:
                self.db.hard = True
                return routput(choice(self.cant_hear_responses))

    def get_display_desc(self, looker, **kwargs):
        # Use this for random information instead of self.db.desc
        response = "In a shape of a bald goblin, the brass door knocker in the center of the red door"
        if self.has("brass ring"):
            response += " holds a ring in its mouth. "
            response += "<<You think it ^ You are sure it ^ You could've sworn it ^ It>> <<just ^ >> winked at you."
        else:
            response += " <<smiles ^ looks>> at you expectantly. "

        return routput(response)

    def get_display_things(self, looker):
        return ""

    def msg(self, text=None, from_obj=None, **kwargs):
        "Custom msg() method reacting to say."
        if from_obj != self:
            # make sure to not repeat what we ourselves said or we'll create a loop
            is_say = False
            is_whisper = False

            try:
                # debug(f"text[0]: {text[0]}, text[1]: {text[1]}")
                # if text comes from a say, `text` is `('say_text', {'type': 'say'})`
                say_text, is_say = text[0], text[1]['type'] == 'say'
                is_whisper = text[1]['type'] == 'whisper'
            except Exception:
                pass

            if is_whisper:
                self.at_say("I'm a little hard of hearing, can you speak up?")
            elif is_say:
                # First get the response (if any)
                response = self.at_heard_say(say_text, from_obj)
                # If there is a response
                if response is not None:
                    self.at_say(response)

        # this is needed if anyone ever puppets this NPC - without it you would never
        # get any feedback from the server (not even the results of look)
        super().msg(text=text, from_obj=from_obj, **kwargs)

    def at_say(self, text):
        obj_name = choice(["The knocker", "The knocker",
                           "The knocker", "The knocker",
                           "The door knocker", "The door knocker",
                           "The goblin-shaped knocker",
                           "The goblin-shaped door knocker"])
        if text.endswith("?"):
            verb = "asks"
        elif text.endswith("!"):
            verb = "exclaims"
        else:
            verb = choice(["says", "replies", "answers",
                           "responds", "says", "says"])

        phrase = f"{obj_name} {verb}, \"{text}\""
        self.location.msg_contents(phrase, exclude=self)

    def at_object_leave(self, moved_obj, target_location, move_type="move", **kwargs):
        """
        If the ring is removed, we make a comment.
        """
        if moved_obj.key == "brass ring" and target_location != self:
            self.at_say("Oh my, that feels better.")
            return True
        return False

    def at_pre_object_receive(self, arriving_object,
                              source_location, **kwargs):
        """
        The knocker only accepts the ring as a gift.
        """
        return True if arriving_object.key == "brass ring" else False

    def at_object_receive(self, obj, source_location,
                          move_type, **kwargs):
        """
        If the ring is returned, we make a comment.
        """
        if obj.key == "brass ring":
            delay(2, self.chewing)
        return True

    def chewing(self):
        """
        Make a muffled comment.
        """
        self.at_say(choice(self.muffled_responses))
