#!/usr/bin/env python

from random import choice, random, randint
from re import split, match, IGNORECASE
from enum import Enum

from evennia import create_script
from evennia.commands.command import InterruptCommand
from evennia.prototypes.spawner import spawn
from evennia.utils import (logger, delay,
                           iter_to_str, int2str,
                           search)
from evennia.utils.search import search_channel

from commands.misc import (CmdSetPuddle,
                           CmdSetStick,
                           CmdSetKnock,
                           CmdSetSmoke,
                           CmdSetRummage,
                           CmdSetDice,
                           CmdSetPress,
                           CmdSetTransform, CmdSetPeer)
from commands.consumables import CmdSetMakeConsumable
from commands.wizards import CmdSetWand, CmdSetScry
from typeclasses.exits import Opener
from utils.word_list import routput, choices, paragraph
from utils.scoring import Scores
from typeclasses.consumables import Litterable
from typeclasses.objects import Object, Recorder
from typeclasses.scripts import KnockScript


class Medal(Object):
    """
    A medal is mostly a marker for showing how far you made.
    """


class CoinPurse(Object):
    """
    A pouch containing the character coins.

    Only the player can see how many coins it contains.
    """
    def at_object_creation(self):
        """
        Set the initial number of coins.
        """
        self.db.gold_amount = 10

    def return_appearance(self, looker, **kwargs):
        """
        Return description based on the 'looker'.
        """
        if self.location == looker:
            if self.has_amount(1):
                return f"Contains {int2str(self.db.gold_amount)} gold coins."
            return "It is empty."
        return "Probably containing coins."

    def how_much(self):
        """
        Return the amount of gold coins in the purse.
        """
        return self.db.gold_amount or 0

    def adjust_amount(self, amount):
        """
        Change the amount of gold in the purse.
        The 'amount' can be a negative number.
        """
        owner = self.location
        if self.how_much() + amount < 0:
            raise InterruptCommand("Not enough coins")

        self.db.gold_amount = self.how_much() + amount
        if self.how_much() == 1:
            owner.msg("You now have one gold coin.")
        else:
            owner.msg(f"You now have {int2str(self.how_much())} gold coins.")

    def has_amount(self, at_least):
        """Return True is pouch has 'at_least' amount of coins."""
        return self.how_much() >= at_least


class Rope(Litterable):
    """
    A rope can be used in particular circumstances.
    """
    def search_exit(self, name):
        x = search.objects(name, typeclass="typeclasses.exits.Exit")
        try:
            return x[0]
        except IndexError:
            return None

    def hut_exits(self):
        return (self.search_exit("hut on stilts"),
                self.search_exit("rope-bound hut"))

    def do_use(self, caller, obj=None):
        if caller.location.key == "Mellow Marsh" or (obj and obj.key == "Homey Hut"):
            # Randomly miss?
            hut1_exit, hut2_exit = self.hut_exits()
            hut1_exit.move_to(None, to_none=True, quiet=True)
            hut2_exit.move_to(caller.location, quiet=True)
            caller.location.add_room_state("bound")

            caller.announce_action("$You() $conj(tie) a knot in $pron(your) vine rope, and $conj(throw) a lasso around the hut, binding the stilts so it can't run away.")
            delay(3, caller.location.msg_contents,
                  "Something tells you that the rope won't last for long.")
            delay(60, self.hut_breaks_free, caller.location)
        else:
            caller.msg("Unclear how to use the rope that way.")

    def hut_breaks_free(self, marsh):
        hut1_exit, hut2_exit = self.hut_exits()
        hut1_exit.move_to(marsh, quiet=True)
        hut2_exit.move_to(None, to_none=True, quiet=True)
        marsh.remove_room_state("bound")
        marsh.msg_contents("The hut, straining against its bounds, finally breaks free to run around the muddy marsh, rendering the vine rope useless.")
        self.delete()


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 Dice(Object):
    """
    Dice that can be rolled.

    A "game" should be able to query the dice to get the results.

    Override the following default properties to adjust the dice:

      @set dice/size = 6
      @set dice/number = 1
    """
    # Defaults ... should be overridden with db:
    number = 1
    size = 6

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

    def roll(self, roller, options):
        size = self.db.size or self.size
        number = self.db.number or self.number
        rolls = [randint(1, size) for d in range(number)]

        self.db.last_roll = sum(rolls)
        prepend = ""
        if number > 1:
            if "sum" in options:
                prepend = prepend + f" The sum is {sum(rolls)}."
            if "high" in options:
                prepend = prepend + f" The highest is {max(rolls)}."
            if "low" in options:
                prepend = prepend + f" The lowest is {min(rolls)}."

        roller.announce_action("$You() $conj(roll) a " + iter_to_str(rolls) +
                               "." + prepend)


class Pipe(Object):
    """
    Simple abstraction for lighting and smoking actions.

    @set pipe/light_msg = "$You() $conj(pull) out, $conj(pack) and $conj(light) $pron(your) pipe."
    @set pipe/smoke_msg = "$You() $conj(lean) back and $conj(<< puff ^ smoke>>) $pron(your) pipe."

    Note that phrases separated by ^ characters and enclosed in << ...
    >> will be randomly selected.
    """
    def at_object_creation(self):
        self.cmdset.add_default(CmdSetSmoke)

    def do_light(self, lighter):
        msg = choices(self.db.light_msg or f"$You() $conj(pack) and $conj(light) $pron(your) {self.name}.")
        lighter.announce_action(msg)
        self.db.is_giving_light = True

    def do_smoke(self, smoker):
        msg = choices(self.db.smoke_msg or f"$You() << $conj(lean) back and ^ >> << $conj(puff) ^ $conj(smoke) >> $pron(your) {self.name}.")
        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.
        ;; The smoke from $your() {self.name} forms a << beautiful ^ swirling ^ spiraling >> ring.
        """)
        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_arrow(self, smoker, details=None):
        self.db._last_option = 'arrow'
        self.db._last_detail = details

        details = details or ""
        msg = paragraph(choices(f"""
        The smoke from $your() {self.name}
        << streaks ^ makes a line ^ makes an arrow ^ shoots out >>
        << pointing to ^ directly at >> {details}!
        """))
        smoker.announce_action(msg)

    def do_monster(self, smoker, details=None):
        self.db._last_option = 'monster'

        # Pre-message:
        smoker.announce_action(paragraph(choices(
            f"""
            $You() $conj(blow) the smoke from $pron(your) {self.name} into a <<swirling mass ^ spiraling vortex ^ spiral ^ dense cloud>>.
            ;;
            The smoke from $your() {self.name} << coalesces ^ coheres ^ swirls together >>.
            """)))

        details = details or choice(["dragon", "flumph", "froghemoth", "serpent"])
        self.db._last_detail = details
        article = "an" if match(r"^[aeiou]", details) else "a"
        msg = routput(
            f"""The smoke << forms ^ forms the shape of ^ begins to resemble ^ looks like ^ becomes >> {article} {details}!""")
        delay(3, smoker.announce_action, msg)
        delay(randint(5, 15), self.do_monster_dissipate, details)

    def do_monster_dissipate(self, monster):
        msg = choices(f"""The << smoke ^ >> {monster} << flies around ^ soars overhead ^ gently floats ^ waves ^ bellows >> before it << eventually ^ finally ^ >> << dissipates ^ disperses ^ looses its shape becoming a puff >>. ;;
        The << smoke ^ >> << monster ^ {monster} >> changes << colors ^ to purple ^ to blue ^ to pink>> before << dissipating ^ dispersing >>.""")
        self.location.location.msg_contents(msg)

class Wood(Litterable):
    "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):
        thrower.score(Scores.throw_stick)
        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.")

    def do_use(self, user, obj):
        """
        Opens a door, and sets a timer to close it.
        """
        if user.location.key == "Lair":
            if not obj or obj.key == "mattress" or obj.key == "trapdoor":
                trapdoor = user.search("trapdoor")
                if trapdoor:
                    if self.key == "ten-foot pole":
                        user.announce_action("$You() $conj(snap) $pron(your) ten-foot pole in half.")
                        name = "two parts of the pole"
                    else:
                        name = self.key

                    user.announce_action(f"$You() $conj(prop) the {name} under the mattress, and $conj(open) the trapdoor. {obj}")
                    trapdoor.do_open()
                    self.delete()

class ClosingDoor(Opener):
    def do_open(self):
        super().do_open()

        delay_time = self.db.open_for or 60
        here = self.db.room
        there = self.db.exit.destination

        here_msg = self.db.room_close_msg or \
            f"The {self.name} closes."
        there_msg = self.db.exit_close_msg or \
            f"The {self.name} closes."

        delay(delay_time, self.do_close)
        delay(delay_time, here.msg_contents, here_msg)
        delay(delay_time, there.msg_contents, there_msg)


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


class Scryer(Object):
    def at_object_creation(self):
        self.cmdset.add_default(CmdSetScry)

    def do_show_room(self, scryer, room_name):
        import typeclasses.rooms
        room = scryer.search(room_name, global_search=True)
        if room and isinstance(room, typeclasses.rooms.Room):
            desc = room.get_full_appearance(scryer).strip()
            others = scryer.characters_here(puppets=True)
            scryer.msg(
                "You gaze into the crystal ball to see the " + desc,
                exclude=others)
        else:
            scryer.msg(f"You can't scry: {room}")


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

    def do_jump(self, player):
        player.score(Scores.jump)
        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 match(full_regex, message, 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))


class BagofJunk(Object):
    """
    A bag of holding with just trivial junk. Perfect for a tweed jacket.
    """
    human_stuff = [
        "wallet",
        "keys",
        "smartphone",
        "handkerchief",
        "pen",
        "notebook",
        "sunglasses",
        "business cards",
        "lip balm",
        "mints",
        "gum",
        "pocket watch",
        "small flashlight",
        "earbuds",
        "uSB drive",
        "travel-sized cologne",
        "hair comb",
        "safety pins",
        "sewing kit",
        "lint roller",
        "tissues",
        "granola bar",
        "a small book",
        "reading glasses",
        "a small umbrella",
        "phone charger",
        "portable battery pack",
        "cough drops",
        "a small notepad",
        "a map",
        "a ticket stub",
        "a receipt",
        "a small first aid kit",
        "a keychain",
        "a small mirror",
        "a hair tie",
        "a small bottle of hand sanitizer",
        "a travel guide",
        "a bookmark",
        "a small camera",
        "a flash drive",
        "a mini tool kit",
        "a stress ball",
        "a small plant seed packet",
        "a whistle",
        "a small notebook for sketches",
        "a travel pillow",
        "a portable fan",
        "a small bottle of water",
        "a snack bag",
        "a pair of gloves",
        "a scarf",
        "a small blanket",
        "a portable game console",
        "a deck of cards",
        "a small puzzle",
        "a travel-sized board game",
        "a notepad for ideas",
        "a small calendar",
        "a set of colored pencils",
        "a small sketchbook",
        "a travel-sized hairbrush",
        "a small bottle of lotion",
        "a portable speaker",
        "a small sewing kit",
        "a pair of earplugs",
        "a small flashlight",
        "a mini umbrella",
        "a small bottle of vinegar",
        "a small bottle of olive oil",
        "a reusable shopping bag",
        "a small notebook for journaling",
        "a small bottle of hot sauce",
        "a small bottle of soy sauce",
        "a small bottle of ketchup",
        "a small bottle of mustard",
        "a small bottle of salad dressing",
        "a small bottle of honey",
        "a small bottle of syrup",
        "a small bottle of jam",
        "a small bottle of peanut butter",
        "a small bottle of jelly",
        "a small bottle of salsa",
        "a small bottle of barbecue sauce",
        "a small bottle of relish",
        "a small bottle of mayonnaise",
        "a small bottle of Worcestershire sauce",
        "a small bottle of teriyaki sauce",
        "a small bottle of chili sauce",
        "a small bottle of curry paste",
        "a small bottle of coconut milk",
        "a small bottle of broth",
        "a small bottle of stock",
        "a small bottle of wine",
        "a small bottle of beer",
        "a small bottle of cider",
        "a small bottle of soda",
        "a small bottle of juice",
    ]
    fey_stuff = [
        "pan flute",
        "an acorn cap",
        "dried leaves",
        "a sprig of thyme",
        "a tiny bell",
        "a piece of string",
        "a handful of wildflower seeds",
        "a feather",
        "a small pouch of herbs",
        "a miniature goat figurine",
        "a piece of bark",
        "a lucky charm",
        "a small stone with a hole",
        "a twig shaped like a heart",
        "a tiny lantern",
        "a scrap of parchment",
        "a small compass",
        "a piece of cheese",
        "a bottle of honey",
        "a small jar of jam",
        "a dried mushroom",
        "a tiny scroll",
        "a thimble",
        "a small wooden carving",
        "a piece of rope",
        "a small pouch of glitter",
        "a tiny drum",
        "a handful of nuts",
        "a piece of flint",
        "a small bellflower",
        "a tiny painting",
        "a small vial of a potion",
        "a piece of candy",
        "a small twig flute",
        "a dried flower",
        "a small notebook",
        "a pencil",
        "a tiny map of the forest",
        "a small bottle of ink",
        "a feather quill",
        "a small pouch of coins",
        "a tiny key",
        "a small wooden spoon",
        "a piece of fabric",
        "a small bell",
        "a small bottle of perfume",
        "a tiny flower crown",
        "a tiny compass",
        "a small wooden flute",
        "a piece of dried fruit",
        "a small jar of fireflies",
        "a tiny drumstick",
        "a small pouch of spices",
        "a small bottle of potion",
        "a small wooden toy",
        "a small feather",
        "a small pouch of seeds",
        "a tiny bellflower",
    ]

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

    def do_rummage(self, owner):
        """
        Rummage around this bag and describe finding an item.
        """

        if self.db.stuff == 'human':
            item = choice(self.human_stuff)
        else:
            item = choice(self.fey_stuff)

        self.db.latest_item = item
        where = self.db._sdesc or self.name

        msg = routput(f"$You() << $conj(scrounge) ^ $conj(rummage) ^ $conj(fish) ^ $conj(rifle) ^ $conj(put) $pron(your) hand >> << around ^ >> in $pron(your) {where}, and << $conj(pull) out ^ $conj(find) ^ $conj(stare) at >>|w{item}|n.")
        owner.announce_action(msg)

    def do_keep(self, keeper, description=None):
        """
        Create an object out of the last thing rummaged.
        """
        if self.db.latest_item:
            where = self.db._sdesc or self.name
            if not description or description == "":
                description = f"An interesting item found in a {where}."

            item = spawn({
                "typeclass": "typeclasses.consumables.Litterable",
                "key": self.db.latest_item,
                "desc": description
            })[0]

            item.location = keeper
            keeper.msg(f"You got {item.key}.")
            self.db.latest_item = None
        else:
            self.location.msg("You must |grummage|n first.")


class IceBreakerHat(Object):
    def at_object_creation(self):
        self.cmdset.add_default(CmdSetMakeConsumable)

    def at_pre_get(self, getter, **kwargs):
        """
        Can't get a producer.
        """
        self.do_make(getter)
        return False

    def do_make(self, picker, **kwargs):
        """
        Create a configurable consumable, given to picker.
        """
        paper = spawn({
            "typeclass": self.db.make_class or kwargs['type']
            if 'type' in kwargs else "typeclasses.readables.Letter",
            "key": self.db.make_name,
            "aliases": self.db.make_aliases,
            "desc": routput(self.db.make_desc),
        })[0]

        paper.db.inside = \
            "The slip of paper reads...\n\n  " + \
            choice([
                "Slide the answer to the following question into the flow of conversation:",
                "Smoothly find out the answer to the following question from another character here:",
                "Tell a short story about yourself that answers the following question:",
            ]) + "\n\n    " + \
            choice([
                "Do you have a nickname? Why were you given it, and what does it mean?",
                "Where is your homeland? What are it's people like?",
                "What do you do in your free time?",
                "What is your hobby?",
                "What is something you carry around constantly? Why?",
                "Do you have a romantic partner? Any exes?",
                "If you inherited a massive pile of gold, how would you spend it?",
                "What do you think about a lot?",
                "What sort of pet would you want to have?",
                "If you could write a book, what would it be about?",
                "Do you have a rival?",
                "What really annoys you?",
                "What are you really passionate about?",
                "What are you afraid of?",
                "What is the most interesting about you?",
                "What is the most embarrassing thing that has happened to you?",
                "If you woke up in jail, why would you be there?",
                "If you could be an animal, which animal would you be and why?",
                # "Describe your personality in ten words or less.",
                "Have you passed up any chances that you now regret? What were they?",
                "What accomplishment are you most proud of?",
                "What was the greatest adventure in your life so far?",
            ])

        paper.location = picker
        picker.announce_action(f"$You() $conj({self.db.make_verb or 'get'}) {self.db.make_name}.")


class Teleporter(Object):
    """
    Teleport a character to a new location.
    Requires the following attributes:

      @set obj/pre_msg = "You press the lever."
      @set obj/post_msg = "The catapult launches you into the air!"
      @set obj/fail_msg = "Other than the click, nothing seems to happen."

      @set obj/destination = $search(Limbo)
      @set obj/details = "01189998819991197253"

    Keep in mind that the details, if given, follows the `press`
    command, but spaces and punctuation are ignored.
    """

    def at_object_creation(self):
        """
        Add the `press` command to this object.
        """
        self.cmdset.add_default(CmdSetPress)

    def trigger(self, victim, details=None):
        """
        Called by the `press` command.
        """

        def announce(victim, msg):
            if match(r"\$", msg):
                victim.announce_action(msg)
            else:
                victim.msg(msg)

        def announce_seq(time_to_delay, victim, msgs):
            lines = split(r" *;; *", msgs)
            for line in lines:
                logger.info(f"time_to_delay={time_to_delay}, announce, victim={victim}, line={line}")
                delay(time_to_delay, announce, victim, line)
                time_to_delay = time_to_delay + 2
            logger.info(f"returning {time_to_delay}")
            return time_to_delay

        time_to_delay = announce_seq(0, victim, self.db.pre_msg)

        deets = self.db.details
        if not deets or (deets and deets == details):
            time_to_delay = announce_seq(time_to_delay, victim, self.db.post_msg)
            delay(time_to_delay, victim.move_to,
                  self.db.destination, move_type="traverse")
        else:
            announce_seq(time_to_delay, victim, self.db.fail_msg)


class MagicRing(Object):
    """
    Ring that can change someone into an object.
    """
    def at_object_creation(self):
        self.cmdset.add_default(CmdSetTransform)

class MagicMonocle(Object):
    """
    Monocle that sees past the 'transform' magic.
    """
    def at_object_creation(self):
        """
        Sets the scoreboard as well as the 'peer' command.
        """
        self.cmdset.add_default(CmdSetPeer)
        self.reset_counts()

    def found_someone(self, found):
        """
        Store a set as an attribute with 'found'.
        """
        the_set = self.attributes.get('found', set())
        the_set.add(found)
        self.attributes.add('found', the_set)

    def failed_at_looking(self):
        """
nv        Increment the failures on the scoreboard.
        """
        self.db.failed = self.db.failed + 1

    def reset_counts(self):
        """
        Resets the scoreboard to 0.
        """
        self.attributes.add('found', set())
        self.attributes.add('failed', 0)

    def report_score(self, seeker=None):
        """
        Reports the score for the object to the room.
        """
        if not seeker:
            seeker = self.location

        room = seeker.location
        name = seeker.sdesc.get()
        room.msg_contents(f"The score for the seeker, {name} is:|/" +
                          f"  - Found: {len(self.db.found)}|/" +
                          f"  - Misses: {self.db.failed}")


class GlobalAlarmClock(Object):
    """
    Clock that announces timers on a public channel.
    """
    def at_object_creation(self):
        """
        Sets up the attributes.
        """
        self.db.channel = "Hide and Seek Game"

    def start_timer(self, seconds=30, start=None, finish=None):
        chan = search_channel(self.db.channel).first()
        if chan:
            if start:
                chan.msg(start)
            delay(seconds, chan.msg, finish)


class Scribe(Recorder):
    """
    Probably invisible script that records room events.

    Set the following properties:

    @set scribe/directory = "transcripts"
    @set scribe/header = "transcripts/header-template.html"
    """
    def msg(self, text=None, from_obj=None, session=None, **kwargs):
        self.record_msg(text, from_obj)
