"""
Room class and mechanics for the Evscaperoom.

This is a special room class that not only depicts the evscaperoom itself, it
also acts as a central store for the room state, score etc. When deleting this,
that particular escaperoom challenge should be gone.

"""

from evennia import DefaultRoom, DefaultCharacter, DefaultObject
from evennia import utils
from evennia.utils.ansi import strip_ansi
from evennia import logger
from evennia.locks.lockhandler import check_lockstring
from evennia.utils.utils import lazy_property, list_to_string
from .objects import EvscaperoomObject
from .commands import CmdSetEvScapeRoom
from .state import StateHandler


class EvscapeRoom(EvscaperoomObject, DefaultRoom):
    """
    The room to escape from.

    """

    def at_object_creation(self):
        """
        Called once, when the room is first created.

        """
        super().at_object_creation()

        # starting state
        self.db.state = None   # name
        self.db.prev_state = None

        # this is used for tagging of all objects belonging to this
        # particular room instance, so they can be cleaned up later
        # this is accessed through the .tagcategory getter.
        self.db.tagcategory = "evscaperoom_{}".format(self.key)

        # room progress statistics
        self.db.stats = {
            "progress": 0,  # in percent
            "score": {},   # reason: score
            "max_score": 100,
            "hints_used": 0,  # total across all states
            "hints_total": 41,
            "total_achievements": 14
        }

        self.cmdset.add(CmdSetEvScapeRoom, permanent=True)

        self.log("Room created and log started.")

    @lazy_property
    def statehandler(self):
        return StateHandler(self)

    @property
    def state(self):
        return self.statehandler.current_state

    def log(self, message, caller=None):
        """
        Log to a file specificially for this room.
        """
        caller = f"[caller.key]: " if caller else ""

        logger.log_file(
            strip_ansi(f"{caller}{message.strip()}"),
            filename=self.tagcategory + ".log")

    def score(self, new_score, reason):
        """
        We don't score individually but for everyone in room together.
        You can only be scored for a given reason once."""
        if reason not in self.db.stats['score']:
            self.log(f"score: {reason} ({new_score}pts)")
            self.db.stats['score'][reason] = new_score

    def progress(self, new_progress):
        "Progress is what we set it to be (0-100%)"
        self.log(f"progress: {new_progress}%")
        self.db.stats['progress'] = new_progress

    def achievement(self, caller, achievement, subtext=""):
        """
        Give the caller a personal achievment. You will only
        ever get one of the same type

        Args:
            caller (Object): The receiver of the achievement.
            achievement (str): The title/name of the achievement.
            subtext (str, optional): Eventual subtext/explanation
                of the achievement.
        """
        achievements = caller.attributes.get(
            "achievements", category=self.tagcategory)
        if not achievements:
            achievements = {}
        if achievement not in achievements:
            self.log(f"achievement: {caller} earned '{achievement}' - {subtext}")
            achievements[achievement] = subtext
            caller.attributes.add("achievements", achievements, category=self.tagcategory)

    def get_all_characters(self):
        """
        Get the player characters in the room.

        Returns:
            chars (Queryset): The characters.

        """
        return DefaultCharacter.objects.filter_family(db_location=self)

    def set_flag(self, flagname):
        self.db.flags[flagname] = True

    def unset_flag(self, flagname):
        if flagname in self.db.flags:
            del self.db.flags[flagname]

    def check_flag(self, flagname):
        return self.db.flags.get(flagname, False)

    def check_perm(self, caller, permission):
        return check_lockstring(caller, f"dummy:perm({permission})")

    def tag_character(self, character, tag, category=None):
        """
        Tag a given character in this room.

        Args:
            character (Character): Player character to tag.
            tag (str): Tag to set.
            category (str, optional): Tag-category. If unset, use room's
                tagcategory.

        """
        category = category if category else self.db.tagcategory
        character.tags.add(tag, category=category)

    def tag_all_characters(self, tag, category=None):
        """
        Set a given tag on all players in the room.

        Args:
            room (EvscapeRoom): The room to escape from.
            tag (str): The tag to set.
            category (str, optional): If unset, will use the room's tagcategory.

        """
        category = category if category else self.tagcategory

        for char in self.get_all_characters():
            char.tags.add(tag, category=category)

    def character_cleanup(self, char):
        """
        Clean all custom tags/attrs on a character.

        """
        if self.tagcategory:
            char.tags.remove(category=self.tagcategory)
            char.attributes.remove(category=self.tagcategory)

    def character_exit(self, char):
        """
        Have a character exit the room - return them to the room menu.

        """
        self.log(f"EXIT: {char} left room")
        from .menu import run_evscaperoom_menu
        self.character_cleanup(char)
        char.location = char.home

        # check if room should be deleted
        if len(self.get_all_characters()) < 1:
            self.delete()

        # we must run menu after deletion so we don't include this room!
        run_evscaperoom_menu(char)

    # Evennia hooks

    def at_object_receive(self, moved_obj, source_location):
        """
        Called when an object arrives in the room. This can be used to
        sum up the situation, set tags etc.

        """
        if utils.inherits_from(moved_obj, "evennia.objects.objects.DefaultCharacter"):
            self.log(f"JOIN: {moved_obj} joined room")
            self.state.character_enters(moved_obj)

    def at_object_leave(self, moved_obj, target_location, **kwargs):
        """
        Called when an object leaves the room; if this is a Character we need
        to clean them up and move them to the menu state.

        """
        if utils.inherits_from(moved_obj, "evennia.objects.objects.DefaultCharacter"):
            self.character_cleanup(moved_obj)
        if len(self.get_all_characters()) <= 1:
            # after this move there'll be no more characters in the room - delete the room!
            self.delete()
            # logger.log_info("DEBUG: Don't delete room when last player leaving")

    def delete(self):
        """
        Delete this room and all items related to it. Only move the players.

        """
        self.db.deleting = True
        for char in self.get_all_characters():
            self.character_exit(char)
        for obj in self.contents:
            obj.delete()
        self.log("END: Room cleaned up and deleted")
        return super().delete()

    def return_appearance(self, looker, **kwargs):
        obj, pos = self.get_position(looker)
        pos = (f"\n|x[{self.position_prep_map[pos]} on "
               f"{obj.get_display_name(looker)}]|n" if obj else "")

        admin_only = ""
        if self.check_perm(looker, "Admin"):
            # only for admins
            objs = DefaultObject.objects.filter_family(
                db_location=self).exclude(id=looker.id)
            admin_only = "\n|xAdmin only: " + \
                list_to_string([obj.get_display_name(looker) for obj in objs])

        return f"{self.db.desc}{pos}{admin_only}"
