#!/usr/bin/env python
"""
Pets

Create interactive 'pets' that consume some sort of food, and need to be 'fed'.
Each level of pet requires more aspects for interaction.

"""

from enum import Enum
from re import sub, split
from time import time

import random

from evennia import TICKER_HANDLER
from evennia.utils import logger, delay
from evennia.utils.gametime import schedule
from evennia.utils.search import search_object

from typeclasses.objects import Object, Listener, AI
from typeclasses.characters import Character
from typeclasses.npcs import Familiar
from commands.pets import CmdPetSet
from commands.misc import CmdSetAntic
from utils.scoring import Scores
from utils.word_list import squish, choices


class Hunger(Enum):
    "States of being for a Pet."
    RAVENOUS = 100
    HUNGRY = 300
    FED = 850
    FULL = 1000


class Pet(Object):
    """
    This is a base class for Pets.
    """
    fullname = None
    pers_pronoun = "it"   # or he/she/they
    poss_pronoun = "its"  # or his/her/them

    # Feed about once a day:
    hungry_rate = -.7

    hunger_states = {
        Hunger.RAVENOUS: [
            "looks crazy with hunger",
        ],
        Hunger.HUNGRY: [
            "looks hungry",
            "seems hungry",
        ],
        Hunger.FED: "looks well",
        Hunger.FULL: [
            "looks content",
            "looks sated",
        ]
    }

    def at_object_creation(self):
        """
        Called when object is first created.
        We set up a ticker to update hunger levels regularly.
        """
        self.db.hunger_level = Hunger.FULL.value

        # Update the state (whatever that may mean), each minute:
        TICKER_HANDLER.add(interval=60, callback=self.update_state)
        super().at_object_creation()

    def hunger(self):
        if self.db.hunger_level < Hunger.RAVENOUS.value:
            return Hunger.RAVENOUS
        if self.db.hunger_level < Hunger.HUNGRY.value:
            return Hunger.HUNGRY
        if self.db.hunger_level < Hunger.FED.value:
            return Hunger.FED
        return Hunger.FULL

    def hunger_appearance(self):
        msgs = self.hunger_states.get(self.hunger())
        if isinstance(msgs, str):
            return f"{self.fullname or self.pers_pronoun} {msgs}"
        return f"{self.fullname or self.pers_pronoun} {random.choice(msgs)}"

    def update_state(self, *args, **kwargs):
        """
        Called by the TickerHandler at regular intervals. Even so, we
        only update at a hungry_rate of the time, picking a random weather message
        when we do. The TickerHandler requires that this hook accepts
        any arguments and keyword arguments (hence the *args, **kwargs
        even though we don't actually use them in this example)
        """
        curr = self.hunger()
        amount = kwargs.get('amount', self.hungry_rate)

        self.db.hunger_level = max(self.db.hunger_level + amount, 0)

        if self.hunger() != curr:
            self.location.msg_contents(f"|w{self.hunger_appearance()}|n\n")

    def return_appearance(self, looker, **kwargs):
        """
        This formats the description of this object based on 'hunger'.
        Called by the 'look' command.

        Args:
            looker (Object): Object doing the looking.
            **kwargs (dict): Arbitrary, optional arguments for users
                overriding the call (unused by default).
        """
        return f"{self.db.desc} {self.hunger_appearance()}"

    def feed(self, giver, obj=None):
        """
        General feed method must be overridden.
        As it just responds with a _no interest_ message.
        """
        if obj:
            giver.msg(f"{self.name} doesn't appear interested in {obj}.")
        else:
            giver.msg(f"{self.name} doesn't appear interested in anything you have.")


# ------------------------------------------------------------
#  Fireplace, both a pet that is hungry and consumes food,
#  but also emits light when it isn't starving.
# ------------------------------------------------------------

class Fire(Pet):
    """
    Fire in this world, is a cute fireplace pet.

    """
    fullname = "The fire in the fireplace"
    hungry_rate = -5
    hunger_states = {
        Hunger.RAVENOUS: [
            "is out. It is dark.",
        ],
        Hunger.HUNGRY: [
            "shows glowing red embers that seems breathe in a memorizing way.",
            "is little more than glowing embers casting shadows on the wall.",
        ],
        Hunger.FED: [
            "dances and shimmies with warmth and light.",
            "crackles and pops with warmth and light."
        ],
        Hunger.FULL: [
            "roars to life, casting a bright light around the room.",
            "burns brightly and is very hot. You feel the need to move back a bit.",
        ]
    }

    def feed_msg(self, giver):
        now = time()
        last_fed = giver.ndb.fire_last_fed  # could be None
        if last_fed and (now - last_fed < 30):
            adj = "some more"
        else:
            adj = "some"

        giver.ndb.fire_last_fed = now

        get_up = ""
        gets_up = ""
        if giver.db.is_sitting:
            get_up = "$conj(get) up and"

        if self.db.hunger_level < 5:
            giver.announce_action(f"$You() {get_up} $conj(start) a fire.")
        else:
            giver.announce_action(f"$You() {get_up} << $conj(feed) ^ $conj(put) {adj} wood on >> the fire << in the fireplace ^ >>.")

    def feed(self, giver, obj=None):
        """
        Feed the fire the default object, wood.
        """
        self.feed_msg(giver)
        self.update_state(giver=giver, amount=300)

    def at_pre_object_receive(self, moved_obj, giver, move_type="move", **kwargs):
        "Burn something in the fireplace."
        if moved_obj.is_typeclass("typeclasses.things.Wood"):
            self.feed_msg(giver)
            self.update_state(giver=giver, amount=400)
        else:
            giver.msg(f"You throw {moved_obj.name} in the fireplace, destroying it.")
        return True


# ----------------------------------------------------------------------
# Migrating Pets move from room to room during particular times

# ----------------------------------------------------------------------
#  Friendly


class Reaction(Enum):
    SCARED = 1
    CONCERNED = 100
    INTERESTED = 300
    FRIENDLY = 850
    ECSTATIC = 1000


class Friendly(Pet):
    """
    This pet keeps track of the characters in the game.
    It has different reactions based on the characters in the room.
    """

    def at_object_creation(self):
        """
        Called when object is first created.
        """
        super().at_object_creation()

        self.cmdset.add(CmdPetSet, persistent=True)

        # We have a list of actions that were spammed to the room:
        self.db.last_actions = []

        # The higher this value the more "spammy" a pet is in making
        # comments in the room:
        self.db.active_amount = 3

        # If set to 'ignores', the pet is more concerned about the
        # 'friendliest' character in Room, otherwise, it is more
        # scared at the stranger:
        self.db.new_character_reaction = "ignores"

    @property
    def friendly_var(self):
        """
        Return variable name on character types use to gauge the
        reaction of this pet towards that character.
        """
        key = self.key.replace(" ", "_")
        return f"{key}_friendly_level"

    def friendly_level(self, character):
        """
        Return reaction level of this pet towards a character.
        """
        varname = self.friendly_var
        return character.attributes.get(varname) or 0

    def friendly_reaction(self, character=None):
        """
        Return reaction enum of this pet towards a character.
        If character not given, then looks at all characters in the room
        """
        if character:
            level = self.friendly_level(character)
            if level < Reaction.SCARED.value:
                return Reaction.SCARED
            if level < Reaction.CONCERNED.value:
                return Reaction.CONCERNED
            if level < Reaction.INTERESTED.value:
                return Reaction.INTERESTED
            if level < Reaction.FRIENDLY.value:
                return Reaction.FRIENDLY
            return Reaction.ECSTATIC
        else:
            if self.db.new_character_reaction == "ignores":
                return self.highest_friendly_reaction()
            return self.lowest_friendly_reaction()

    @property
    def local_characters(self):
        """
        Return a list of all Characters in the room with the Pet.
        """
        return [c for c in self.location.contents
                if c.is_typeclass("typeclasses.characters.Character")]

    def lowest_friendly_reaction(self):
        """
        Return reaction of this pet to the least friendliest
        character(s) in the area (room) that this pet resides.
        Returns a tuple, Reaction and a list of characters.
        """
        # State is a tuple of the level and the character:
        level = Reaction.ECSTATIC
        characters = []

        for c in self.local_characters:
            this_level = self.friendly_reaction(c)
            if this_level.value < level.value:
                level = this_level
                characters = [c]
            if this_level.value == level.value:
                characters += [c]
        return (level, characters)

    def highest_friendly_reaction(self):
        """
        Return reaction of this pet to the friendliest
        character(s) in the area (room) that this pet resides.
        Returns a tuple, Reaction and a list of characters.
        """
        # State is a tuple of the level and the character:
        level = Reaction.SCARED
        characters = []

        for c in self.local_characters:
            this_level = self.friendly_reaction(c)
            if this_level.value > level.value:
                level = this_level
                characters = [c]
            if this_level.value == level.value:
                characters += [c]
        return (level, characters)

    def adjust_all(self, amount):
        """
        Adjusts reaction level to all characters that have
        interacted with this pet, whether they are near it or not.
        This is essentially a loneliness measure, for out-of-sight,
        out-of-mind.
        """
        for c in Character.objects.get_objs_with_attr(self.friendly_var):
            self.adjust_character(c, amount)

    def adjust_all_locally(self, amount):
        """
        Adjusts reaction level to all characters in the room
        with this pet. Hanging out with the pet should be helpful.
        """
        for c in self.local_characters:
            self.adjust_character(c, amount)

    def adjust_character(self, character, amount):
        """
        Adjusts the reaction to 'character' by an 'amount.
        Note that this should never go below zero.
        """
        new_val = self.friendly_level(character) + amount
        if new_val < 0:
            new_val = 0
        character.attributes.add(self.friendly_var, new_val)

    def return_appearance(self, looker, **kwargs):
        """
        Called by the 'look' command. This formats the description
        of this object based on 'reaction', and the character's
        _friendly_ level.

        Args:
            looker (Object): Object doing the looking.
        """
        # looking at the friendly pets makes them nervous... just a little:
        self.adjust_character(looker, -1)

        (level, loves) = self.highest_friendly_reaction()
        loved = loves[0]

        if loved == looker:
            subs = ("you", "your")
        else:
            subs = (loved.db._sdesc, loved.db._sdesc + "'s")

        if level.value > Reaction.ECSTATIC.value and self.db.ecstatic_msg:
            suffix = choices(self.db.ecstatic_msg, *subs)
        elif level.value > Reaction.FRIENDLY.value and self.db.friendly_msg:
            suffix = choices(self.db.friendly_msg, *subs)
        elif level.value > Reaction.INTERESTED.value and self.db.interested_msg:
            suffix = choices(self.db.interested_msg, *subs)
        elif level.value > Reaction.CONCERNED.value and self.db.concerned_msg:
            suffix = choices(self.db.concerned_msg, *subs)
        else:
            suffix = choices(self.db.scared_msg, *subs)

        return self.db.desc + " " + loved.gendered_text(suffix)

    def update_state(self, *args, **kwargs):
        """
        Call regularly to adjust the pet's reaction state.

        Change the state associated with _all_ characters, as well
        as characters in the area, based on 'loneliness_amount' and
        'shyness_amount' respectively.

        Then, if 'active_amount' is triggered, call 'do_action'.

        """
        super().update_state(*args, **kwargs)
        self.adjust_all(self.db.loneliness_amount or -1)
        self.adjust_all_locally(self.db.shyness_amount or 5)

        # How spammy do we want the pet to be?
        if random.randint(0, 100) < (self.db.active_amount or 3):
            self.do_action()

    def do_action(self):
        """Do something based on the highest friendly level is the
        same area.
        """
        def choose_action(actions):
            num = len(split(r" *;; *", actions))
            last = self.db.last_actions
            msg = choices(actions)
            if msg and msg not in last:
                self.db.last_actions.append(msg)
                limit_num = -num // 2
                self.db.last_actions = self.db.last_actions[limit_num:]
            return msg

        msg = None
        (level, chars) = self.friendly_reaction()
        if chars and len(chars) > 0:
            focus = random.choice(chars)

            if level == Reaction.SCARED:
                msg = choose_action(self.db.scared_actions)
            elif level == Reaction.CONCERNED:
                msg = choose_action(self.db.concerned_actions)
            elif level == Reaction.INTERESTED:
                msg = choose_action(self.db.interested_actions)
            elif level == Reaction.FRIENDLY:
                msg = choose_action(self.db.friendly_actions)
            else:
                # If we have an ecstatic message, use it otherwise,
                # grab the friendly:
                msg = choose_action(self.db.ecstatic_actions or self.db.friendly_actions)
            if focus and msg:
                focus.announce_action(msg)

    def pet_response(self, petter):
        """
        Called with 'petter' attempts to 'pet' this.
        Reaction should be based on petter reaction state.
        """
        match self.friendly_reaction(petter):
            case Reaction.SCARED:
                msg = self.db.pet_scared_response \
                    or f"The {self.name} doesn't let $you() pet it."
                self.adjust_character(petter, self.db.pet_scared_adjust or 0)
            case Reaction.CONCERNED:
                msg = self.db.pet_concerned_response or self.db.pet_scared_response \
                    or f"The {self.name} doesn't let $you() pet it."
                self.adjust_character(petter, self.db.pet_concerned_adjust or 5)
            case Reaction.INTERESTED:
                msg = self.db.pet_interested_response \
                    or self.db.pet_concerned_response or self.db.pet_scared_response \
                    or f"The {self.name} doesn't let $you() pet it."
                self.adjust_character(petter, self.db.pet_interested_adjust or 8)
            case Reaction.FRIENDLY:
                msg = self.db.pet_friendly_response or self.db.pet_interested_response \
                    or self.db.pet_concerned_response or self.db.pet_scared_response \
                    or f"The {self.name} doesn't let $you() pet it."
                self.adjust_character(petter, self.db.pet_friendly_adjust or 10)
            case Reaction.ECSTATIC:
                msg = self.db.pet_ecstatic_response or self.db.pet_friendly_response \
                    or self.db.pet_interested_response or self.db.pet_concerned_response \
                    or self.db.pet_scared_response \
                    or f"The {self.name} doesn't let $you() pet it."
                self.adjust_character(petter, self.db.pet_ecstatic_adjust or 15)
        if msg:
            petter.announce_action(msg, self.name)
        else:
            petter.announce_action(f"$You() $conj(pet) {self.name}.")


# The key attribute is the Listener mixin:
class WeeBeastie(Friendly, Familiar, Listener, AI):
    """
    The stoat that lives in Dabbler's house, is a finicky eater.

    @set beastie/personality = "wee-beastie"
    @set beastie/personality_file = "personalities/wee-beastie.md"
    """
    def at_object_creation(self):
        "Called when this pet is first created."
        super().at_object_creation()
        self.cmdset.add(CmdSetAntic, persistent=True)

    def other_say(self, speaker, speech):
        if len(self.characters_here()) == 1:
            self.other_sayto(speaker, speech)

    def other_sayto(self, speaker, speech):
        "Override to return a string in response to message."
        if speech:
            reply = self.think(speaker, speech)
            logger.info(f"Reply: {reply}")
            self.process_thoughts(reply)
        else:
            delay(3, self.execute_cmd, f"emote /me purrs.")

    def other_arrive(self, character):
        """
        Greet a character when it arrives.
        """
        if character.key == "Dabbler":
            delay(4, self.location.msg_contents,
                  choices("""
                      The << wee ^ >> beastie << jumps ^ leaps ^ hops >> << up ^ >> on the back of << an overstuffed ^ a >> chair, and << bobs it head ^ purrs >>. ;;
                      A << white ^ >> ball of fur << jumps ^ leaps ^ hops >> down from a chair and climbs up the << old ^ >> gnome to get close<< to his face ^ >>."""))
        else:
            delay(4, self.location.msg_contents,
                  choices(f"""
                      The << furry ^ >> white << animal ^ creature ^ beastie >> asleep on one of the chairs, << lifts an eyelid to look at the {character.get_name()} before returning to its nap ^ wakes to watch the {character.get_name()}. >>"""))

    def other_sit(self, character):
        if character.key == "Dabbler":
            delay(4, self.location.msg_contents,
                  choices(f"""
                      The << wee ^ >> beastie << jumps ^ leaps ^ hops >> << up ^ >> into the lap of the {character.get_name()}, and << falls asleep ^ snuggles into a ball ^ purrs >>. ;;
                  The << wee ^ >> beastie pulls up a << brown ^ >> blanket over {character.get_name()}'s lap."""))

    def feed(self, feeder, item=None):
        """
        Feeding the beast. If item is None, we choose something
        the character has, and go with that...
        """
        # Categorize items that can be used to feed the beast:
        def is_some(item, name):
            if item and item.key == name:
                return item
            if not item:
                item = feeder.has(name)
                if item:
                    return item[0]

        def is_edible(item):
            return is_some(item, "yellow flower") or \
                is_some(item, "candy") or \
                is_some(item, "turkish delight")

        # Based on the reaction to the feeder, the adjectives may alter:
        noun = "The " + random.choice(["wee", "furry", "white", "adorable"]) + " beastie"

        match self.friendly_reaction(feeder):
            case Reaction.SCARED:
                how_sniff = "carefully"
                how_eat = "curiously"
                and_then = "nonchalantly walks away"
            case Reaction.CONCERNED:
                how_sniff = "gently"
                how_eat = "gingerly"
                and_then = "indifferently returns to its nap"
            case Reaction.INTERESTED:
                how_sniff = "quickly"
                how_eat = "curiously"
                and_then = "gingerly sniffs to see what else $you() might have"
            case _:
                how_sniff = "excitedly"
                how_eat = random.choice(["eagerly", "gladly", "vigorously"])
                and_then = random.choice([
                    "gives $pron(you,op) a cute lick as if to say, Thank you",
                    "purrs in appreciation for the treat",
                    "rubs its wee widdle head under $pron(your,op) chin in gratitude",
                ])

        edible = is_edible(item)
        if edible:
            msg = f"{noun} {how_sniff} sniffs $your() << hand holding a ^>> {edible}. It {how_eat} eats it, and {and_then}."
            self.adjust_character(feeder, 100)
            edible.delete()
            feeder.announce_action(msg)
        else:
            msg = f"{noun} doesn't appear interested in anything you have."
            feeder.msg(msg)


class BHB(Friendly, Listener, AI):
    def return_appearance(self, looker):
        if self.db.is_awake:
            return super().return_appearance(looker)
        else:
            return "It currently slumbers on its huge mattress."

    def other_sayto(self, speaker, speech):
        "Override to return a string in response to message."
        reply = self.think(speaker, speech)
        self.process_thoughts(reply)

    def update_state(self, *args, **kwargs):
        msg = None
        wake_hour = self.db.wake_hour or 8
        sleep_hour = self.db.sleep_hour or 20

        (minute, hour, tod, season) = self.location.get_time()

        if hour >= wake_hour and hour < sleep_hour:
            self.db.is_awake = True
        else:
            self.db.is_awake = False

        noun = random.choice(["beast", "BHB", "monstrous beast",
                              "big, hairy beast"])

        if hour == wake_hour - 1:
            if minute == 53:
                msg = f"The {noun} begins to stir from slumber."
            elif minute == 56:
                msg = f"The {noun} rises and stretches ..."
            elif minute == 58:
                msg = f"The {noun} lets loose a big yawn."

        if hour == sleep_hour - 1:
            if minute == 53:
                msg = f"The {noun} look up at the darkening sky."
            elif minute == 56:
                msg = f"The {noun} blinks. It looks tired."
            elif minute == 58:
                msg = f"The {noun} lets loose a big yawn."

        meadow = search_object("mp05").first()
        cave = search_object("mp07").first()

        if 'portal_open' in self.location.room_states:
            msg = f"The {noun} becomes frightened of the flying lights."
            self.location.msg_contents(msg)
            self.move_to(cave, move_type="traverse")
            return

        if self.db.is_awake and self.location != meadow:
            self.move_to(meadow, move_type="traverse")
        elif not self.db.is_awake and self.location != cave:
            self.move_to(cave, move_type="traverse")

        if msg and self.location:
            self.location.msg_contents(msg)
        else:
            # This might call the do_action():
            super().update_state(*args, **kwargs)

    def do_action(self):
        """
        We only override the Friendly 'do_action' if the beast is
        asleep on its huge pilla'.
        """
        if self.db.is_awake:
            super().do_action()
        else:
            msg = choices(self.db.sleeping_actions)
            if msg != self.db.last_actions:
                self.db.last_actions.append(msg)
                # Limit this list so it doesn't grow too large:
                self.db.last_actions = self.db.last_actions[-5:]

                self.location.msg_contents(msg)

    def feed(self, feeder, item=None):
        """
        Feeding the beast. If item is None, we choose something
        the character has, and go with that...
        """
        # Categorize items that can be used to feed the beast:
        def is_berry(item):
            return (not item and feeder.has("berries")) or (item and item.key == 'berries')
        def is_scone(item):
            return (not item and feeder.has("scone")) or (item and item.key == 'scone')

        # Based on the reaction to the feeder, the adjectives may alter:
        noun = "The " + random.choice(["huge", "big, hairy"]) + " beast"

        match self.friendly_reaction(feeder):
            case Reaction.SCARED:
                how_sniff = "carefully"
                how_eat = "cautiously"
                and_then = "an then runs away to the edge of the meadow"
            case Reaction.CONCERNED:
                how_sniff = "cautiously"
                how_eat = "gingerly"
                and_then = "slowly backs away to a safe distance"
            case Reaction.INTERESTED:
                how_sniff = "curiously"
                how_eat = "quickly"
                and_then = "waits to see what else $you() might have"
            case _:
                how_sniff = "eagerly"
                how_eat = "excitedly"
                and_then = "gives $pron(you,op) a big lick as if to say, Thank you"

        if is_scone(item):
            msg = f"{noun} {how_sniff} sniffs $your() outstretched arm holding a scone. It {how_eat} eats it, and {and_then}."
            self.adjust_character(feeder, 100)
            feeder.has('scone').delete()
            feeder.score(Scores.feed_bhb)
        elif is_berry(item):
            msg = f"{noun} {how_sniff} sniffs $your() outstretched arm with a handful of berries. It {how_eat} eats them, and {and_then}."
            self.adjust_character(feeder, 40)
            feeder.has('berries').delete()
            feeder.score(Scores.feed_bhb)
        else:
            msg = f"{noun} doesn't appear interested in anything you have."

        if msg:
            feeder.announce_action(msg)

    def thrown_stick(self, thrower):
        """
        Called when thrower is in the vicinity of the beast, and
        throws a stick they may have.
        """
        match self.friendly_reaction(thrower):
            case Reaction.SCARED:
                msg = "The beast runs away when $you() throw the stick."
            case Reaction.CONCERNED:
                msg = "The beast walks over to the stick $you() threw, sniffs it, and backs away."
                self.adjust_character(thrower, 5)
            case Reaction.INTERESTED:
                msg = choices("""
                The beast <<runs ^ hurries>> at the stick, then looks at $you(), <<wondering ^ curious as to ^ pondering>> what to do with a stick the flies back to its owner. ;;
                The beast <<rushes ^ runs at>> the stick, and then veers off, thundering around the <<field ^ meadow>>. The stick returns to $your() hand.
                """)
                self.adjust_character(thrower, 30)

            case _:
                msg = choices("""
                The <<big hairy ^ >> beast <<dashes ^ runs ^ jumps ^ leaps into the air ^ leaps>> and <<catches ^ snags ^ grabs>> the stick in midair, and drops it in front of $you(). ;;
                The <<big hairy ^ >> beast <<gallops ^ trumbles ^ trots>> over to the stick and <<dances ^ prances ^ hops>> around the stick before bringing it back to $you(). ;;
                The <<big hairy ^ >> beast <<gallops ^ trumbles ^ trots>> over to the stick and <<dances ^ prances ^ hops>> around the stick before bringing it back to $you().""")
                self.adjust_character(thrower, 10)
        if msg:
            thrower.announce_action(msg)
