"""
Player (OOC) commands. These are stored on the Player object
and self.caller is thus always a Player, not an Object/Character.

These commands go in the PlayerCmdset and are accessible also
when puppeting a Character (although with lower priority)

These commands use the MuxCommandOOC parent that makes sure
to setup caller correctly. The self.character can be used to
access the character when these commands are triggered with
a connected character (such as the case of the @ooc command), it
is None if we are OOC.

Note that under MULTISESSION_MODE=2, Player- commands should use
self.msg() and similar methods to reroute returns to the correct
method. Otherwise all text will be returned to all connected sessions.

"""
import time
from django.conf import settings
from src.server.sessionhandler import SESSIONS
from src.commands.default.muxcommand import MuxPlayerCommand
from src.utils import utils, create, search, prettytable

from settings import MAX_NR_CHARACTERS, MULTISESSION_MODE
# limit symbol import for API
__all__ = ("CmdOOCLook", "CmdIC", "CmdOOC", "CmdPassword", "CmdQuit", "CmdCharCreate",
           "CmdEncoding", "CmdSessions", "CmdWho", "CmdColorTest", "CmdQuell")

# force max nr chars to 1 if mode is 0 or 1
MAX_NR_CHARACTERS = MULTISESSION_MODE < 2 and 1 or MAX_NR_CHARACTERS
BASE_PLAYER_TYPECLASS = settings.BASE_PLAYER_TYPECLASS

PERMISSION_HIERARCHY = settings.PERMISSION_HIERARCHY
PERMISSION_HIERARCHY_LOWER = [perm.lower() for perm in PERMISSION_HIERARCHY]

class CmdOOCLook(MuxPlayerCommand):
    """
    ooc look

    Usage:
      look

    Look in the ooc state.
    """

    #This is an OOC version of the look command. Since a
    #Player doesn't have an in-game existence, there is no
    #concept of location or "self". If we are controlling
    #a character, pass control over to normal look.

    key = "look"
    aliases = ["l", "ls"]
    locks = "cmd:all()"
    help_category = "General"

    def look_target(self):
        "Hook method for when an argument is given."
        player = self.caller
        key = self.args.lower()
        chars = dict((utils.to_str(char.key.lower()), char) for char in player.db._playable_characters)
        looktarget = chars.get(key)
        if looktarget:
            self.msg(looktarget.return_appearance(player))
        else:
            self.msg("No such character.")
        return

    def no_look_target(self):
        "Hook method for default look without a specified target"
        # caller is always a player at this point.
        player = self.caller
        sessid = self.sessid
        # get all our characters and sessions
        characters = player.db._playable_characters
        sessions = player.get_all_sessions()
        is_su = player.is_superuser

        # text shown when looking in the ooc area
        string = "Account {g%s{n (you are Out-of-Character)" % (player.key)

        nsess = len(sessions)
        string += nsess == 1 and "\n\n{wConnected session:{n" or "\n\n{wConnected sessions (%i):{n" % nsess
        for isess, sess in enumerate(sessions):
            csessid = sess.sessid
            addr = "%s (%s)" % (sess.protocol_key, isinstance(sess.address, tuple) and str(sess.address[0]) or str(sess.address))
            string += "\n %s %s" % (sessid == csessid and "{w%s{n" % (isess + 1) or (isess + 1), addr)
        string += "\n\n {whelp{n - more commands"
        string += "\n {wooc <Text>{n - talk on public channel"

        if is_su or len(characters) < MAX_NR_CHARACTERS:
            if not characters:
                string += "\n\n You don't have any character yet. See {whelp @charcreate{n for creating one."
            else:
                string += "\n {w@charcreate <name> [=description]{n - create new character"

        if characters:
            string_s_ending = len(characters) > 1 and "s" or ""
            string += "\n {w@ic <character>{n - enter the game ({w@ooc{n to get back here)"
            if is_su:
                string += "\n\nAvailable character%s (%i/unlimited):" % (string_s_ending, len(characters))
            else:
                string += "\n\nAvailable character%s%s:"  % (string_s_ending,
                                                         MAX_NR_CHARACTERS > 1 and " (%i/%i)" % (len(characters), MAX_NR_CHARACTERS) or "")

            for char in characters:
                csessid = char.sessid
                if csessid:
                    # character is already puppeted
                    sess = player.get_session(csessid)
                    sid = sess in sessions and sessions.index(sess) + 1
                    if sess and sid:
                        string += "\n - {G%s{n [%s] (played by you in session %i)" % (char.key, ", ".join(char.permissions), sid)
                    else:
                        string += "\n - {R%s{n [%s] (played by someone else)" % (char.key, ", ".join(char.permissions))
                else:
                    # character is "free to puppet"
                    string += "\n - %s [%s]" % (char.key, ", ".join(char.permissions))
        string = ("-" * 68) + "\n" + string + "\n" + ("-" * 68)
        self.msg(string)

    def func(self):
        "implement the ooc look command"
        if MULTISESSION_MODE < 2:
            # only one character allowed
            string = "You are out-of-character (OOC).\nUse {w@ic{n to get back into the game."
            self.msg(string)
            return
        if utils.inherits_from(self.caller, "src.objects.objects.Object"):
            # An object of some type is calling. Use default look instead.
            super(CmdOOCLook, self).func()
        elif self.args:
            self.look_target()
        else:
            self.no_look_target()


class CmdCharCreate(MuxPlayerCommand):
    """
    Create a character

    Usage:
      @charcreate <charname> [= desc]

    Create a new character, optionally giving it a description. You
    may use upper-case letters in the name - you will nevertheless
    always be able to access your character using lower-case letters
    if you want.
    """
    key = "@charcreate"
    locks = "cmd:all()"
    help_category = "General"

    def func(self):
        "create the new character"
        player = self.caller
        if not self.args:
            self.msg("Usage: @charcreate <charname> [= description]")
            return
        key = self.lhs
        desc = self.rhs
        if not player.is_superuser and \
            (player.db._playable_characters and
                len(player.db._playable_characters) >= MAX_NR_CHARACTERS):
            self.msg("You may only create a maximum of %i characters." % MAX_NR_CHARACTERS)
            return
        # create the character
        from src.objects.models import ObjectDB

        default_home = ObjectDB.objects.get_id(settings.CHARACTER_DEFAULT_HOME)
        typeclass = settings.BASE_CHARACTER_TYPECLASS
        permissions = settings.PERMISSION_PLAYER_DEFAULT

        new_character = create.create_object(typeclass, key=key, location=default_home,
                                             home=default_home, permissions=permissions)
        # only allow creator (and immortals) to puppet this char
        new_character.locks.add("puppet:id(%i) or pid(%i) or perm(Immortals) or pperm(Immortals)" %
                                (new_character.id, player.id))
        player.db._playable_characters.append(new_character)
        if desc:
            new_character.db.desc = desc
        else:
            new_character.db.desc = "This is a Player."
        self.msg("Created new character %s. Use {w@ic %s{n to enter the game as this character." % (new_character.key, new_character.key))


class CmdIC(MuxPlayerCommand):
    """
    Switch control to an object

    Usage:
      @ic <character>

    Go in-character (IC) as a given Character.

    This will attempt to "become" a different object assuming you have
    the right to do so. Note that it's the PLAYER character that puppets
    characters/objects and which needs to have the correct permission!

    You cannot become an object that is already controlled by another
    player. In principle <character> can be any in-game object as long
    as you the player have access right to puppet it.
    """

    key = "@ic"
    locks = "cmd:all()" # must be all() or different puppeted objects won't be able to access it.
    aliases = "@puppet"
    help_category = "General"

    def func(self):
        """
        Main puppet method
        """
        player = self.caller
        sessid = self.sessid

        new_character = None
        if not self.args:
            new_character = player.db._last_puppet
            if not new_character:
                self.msg("Usage: @ic <character>")
                return
        if not new_character:
            # search for a matching character
            new_character = search.object_search(self.args)
            if new_character:
                new_character = new_character[0]
            else:
                self.msg("That is not a valid character choice.")
                return
        # permission checks
        if player.get_puppet(sessid) == new_character:
            self.msg("{RYou already act as {c%s{n." % new_character.name)
            return
        if new_character.player:
            # may not puppet an already puppeted character
            if new_character.sessid and new_character.player == player:
                # as a safeguard we allow "taking over chars from your own sessions.
                player.msg("{c%s{n{R is now acted from another of your sessions.{n" % (new_character.name), sessid=new_character.sessid)
                player.unpuppet_object(new_character.sessid)
                self.msg("Taking over {c%s{n from another of your sessions." % new_character.name)
            elif new_character.player != player and new_character.player.is_connected:
                self.msg("{c%s{r is already acted by another player.{n" % new_character.name)
                return
        if not new_character.access(player, "puppet"):
            # main acccess check
            self.msg("{rYou may not become %s.{n" % new_character.name)
            return
        if player.puppet_object(sessid, new_character):
            player.db._last_puppet = new_character
        else:
            self.msg("{rYou cannot become {C%s{n." % new_character.name)

class CmdOOC(MuxPlayerCommand):
    """
    go ooc

    Usage:
      @ooc

    Go out-of-character (OOC).

    This will leave your current character and put you in a incorporeal OOC state.
    """

    key = "@ooc"
    locks = "cmd:all()" # this must be all(), or different puppeted objects won't be able to access it.
    aliases = "@unpuppet"
    help_category = "General"

    def func(self):
        "Implement function"

        player = self.caller
        sessid = self.sessid

        old_char = player.get_puppet(sessid)
        if not old_char:
            string = "You are already OOC."
            self.msg(string)
            return

        player.db._last_puppet = old_char
        # save location as if we were disconnecting from the game entirely.
        if old_char.location:
            old_char.location.msg_contents("%s has left the game." % old_char.key, exclude=[old_char])

        # disconnect
        if player.unpuppet_object(sessid):
            self.msg("\n{GYou go OOC.{n\n")
            player.execute_cmd("look", sessid=sessid)
        else:
            raise RuntimeError("Could not unpuppet!")

class CmdSessions(MuxPlayerCommand):
    """
    check connected session(s)

    Usage:
      @sessions

    Lists the sessions currently connected to your account.

    """
    key = "@sessions"
    locks = "cmd:all()"
    help_category = "General"

    def func(self):
        "Implement function"
        player = self.caller
        sessions = player.get_all_sessions()

        table = prettytable.PrettyTable(["{wsessid", "{wprotocol", "{whost", "{wpuppet/character", "{wlocation"])
        for sess in sorted(sessions, key=lambda x:x.sessid):
            sessid = sess.sessid
            char = player.get_puppet(sessid)
            table.add_row([str(sessid), str(sess.protocol_key),
                           type(sess.address)==tuple and sess.address[0] or sess.address,
                           char and str(char) or "None",
                           char and str(char.location) or "N/A"])
        string = "{wYour current session(s):{n\n%s" % table
        self.msg(string)

class CmdWho(MuxPlayerCommand):
    """
    who

    Usage:
      who
      doing

    Shows who is currently online. Doing is an alias that limits info
    also for those with all permissions.
    """

    key = "who"
    aliases = "doing"
    locks = "cmd:all()"

    def func(self):
        """
        Get all connected players by polling session.
        """

        player = self.caller
        session_list = SESSIONS.get_sessions()

        session_list = sorted(session_list, key=lambda o: o.player.key)

        if self.cmdstring == "doing":
            show_session_data = False
        else:
            show_session_data = player.check_permstring("Immortals") or player.check_permstring("Wizards")

        nplayers = (SESSIONS.player_count())
        if show_session_data:
            table = prettytable.PrettyTable(["{wPlayer Name","{wOn for", "{wIdle", "{wRoom", "{wCmds", "{wProtocol", "{wHost"])
            for session in session_list:
                if not session.logged_in: continue
                delta_cmd = time.time() - session.cmd_last_visible
                delta_conn = time.time() - session.conn_time
                plr_pobject = session.get_puppet()
                plr_pobject = plr_pobject or session.get_player()
                table.add_row([utils.crop(plr_pobject.name, width=25),
                               utils.time_format(delta_conn, 0),
                               utils.time_format(delta_cmd, 1),
                               hasattr(plr_pobject, "location") and plr_pobject.location.key or "None",
                               session.cmd_total,
                               session.protocol_key,
                               isinstance(session.address, tuple) and session.address[0] or session.address])
        else:
            table = prettytable.PrettyTable(["{wPlayer name", "{wOn for", "{Idle"])
            for session in session_list:
                if not session.logged_in: continue
                delta_cmd = time.time() - session.cmd_last_visible
                delta_conn = time.time() - session.conn_time
                plr_pobject = session.get_puppet()
                plr_pobject = plr_pobject or session.get_player()
                table.add_row([utils.crop(plr_pobject.name, width=25),
                               utils.time_format(delta_conn, 0),
                               utils.time_format(delta_cmd, 1)])

        isone = nplayers == 1
        string = "{wPlayers:{n\n%s\n%s unique account%s logged in." % (table, "One" if isone else nplayers, "" if isone else "s")
        self.msg(string)


class CmdEncoding(MuxPlayerCommand):
    """
    encoding - set a custom text encoding

    Usage:
      @encoding/switches [<encoding>]

    Switches:
      clear - clear your custom encoding


    This sets the text encoding for communicating with Evennia. This is mostly an issue only if
    you want to use non-ASCII characters (i.e. letters/symbols not found in English). If you see
    that your characters look strange (or you get encoding errors), you should use this command
    to set the server encoding to be the same used in your client program.

    Common encodings are utf-8 (default), latin-1, ISO-8859-1 etc.

    If you don't submit an encoding, the current encoding will be displayed instead.
  """

    key = "@encoding"
    aliases = "@encode"
    locks = "cmd:all()"

    def func(self):
        """
        Sets the encoding.
        """
        player = self.caller

        if 'clear' in self.switches:
            # remove customization
            old_encoding = player.db.encoding
            if old_encoding:
                string = "Your custom text encoding ('%s') was cleared." % old_encoding
            else:
                string = "No custom encoding was set."
            del player.db.encoding
        elif not self.args:
            # just list the encodings supported
            pencoding = player.db.encoding
            string = ""
            if pencoding:
                string += "Default encoding: {g%s{n (change with {w@encoding <encoding>{n)" % pencoding
            encodings = settings.ENCODINGS
            if encodings:
                string += "\nServer's alternative encodings (tested in this order):\n   {g%s{n" % ", ".join(encodings)
            if not string:
                string = "No encodings found."
        else:
            # change encoding
            old_encoding = player.db.encoding
            encoding = self.args
            player.db.encoding = encoding
            string = "Your custom text encoding was changed from '%s' to '%s'." % (old_encoding, encoding)
        self.msg(string.strip())

class CmdPassword(MuxPlayerCommand):
    """
    @password - set your password

    Usage:
      @password <old password> = <new password>

    Changes your password. Make sure to pick a safe one.
    """
    key = "@password"
    locks = "cmd:all()"

    def func(self):
        "hook function."

        player = self.caller
        if not self.rhs:
            self.msg("Usage: @password <oldpass> = <newpass>")
            return
        oldpass = self.lhslist[0] # this is already stripped by parse()
        newpass = self.rhslist[0] #               ''
        try:
            uaccount = player.user
        except AttributeError:
            self.msg("This is only applicable for players.")
            return
        if not uaccount.check_password(oldpass):
            self.msg("The specified old password isn't correct.")
        elif len(newpass) < 3:
            self.msg("Passwords must be at least three characters long.")
        else:
            uaccount.set_password(newpass)
            uaccount.save()
            self.msg("Password changed.")

class CmdQuit(MuxPlayerCommand):
    """
    quit

    Usage:
      @quit

    Switch:
      all - disconnect all connected sessions

    Gracefully disconnect your current session from the
    game. Use the /all switch to disconnect from all sessions.
    """
    key = "@quit"
    locks = "cmd:all()"

    def func(self):
        "hook function"
        player = self.caller

        if 'all' in self.switches:
            player.msg("{RQuitting{n all sessions. Hope to see you soon again.", sessid=self.sessid)
            for session in player.get_all_sessions():
                player.disconnect_session_from_player(session.sessid)
        else:
            nsess = len(player.get_all_sessions())
            if nsess == 2:
                player.msg("{RQuitting{n. One session is still connected.", sessid=self.sessid)
            elif nsess > 2:
                player.msg("{RQuitting{n. %i session are still connected." % (nsess-1), sessid=self.sessid)
            else:
                # we are quitting the last available session
                player.msg("{RQuitting{n. Hope to see you soon again.", sessid=self.sessid)
            player.disconnect_session_from_player(self.sessid)



class CmdColorTest(MuxPlayerCommand):
    """
    testing colors

    Usage:
      @color ansi|xterm256

    Print a color map along with in-mud color codes, while testing what is supported in your client.
    Choices are 16-color ansi (supported in most muds) or the 256-color xterm256 standard.
    No checking is done to determine your client supports color - if not you will
    see rubbish appear.
    """
    key = "@color"
    locks = "cmd:all()"
    help_category = "General"

    def table_format(self, table):
       """
       Helper method to format the ansi/xterm256 tables.
       Takes a table of columns [[val,val,...],[val,val,...],...]
       """
       if not table:
           return [[]]

       extra_space = 1
       max_widths = [max([len(str(val)) for val in col]) for col in table]
       ftable = []
       for irow in range(len(table[0])):
           ftable.append([str(col[irow]).ljust(max_widths[icol]) + " " * extra_space
                          for icol, col in enumerate(table)])
       return ftable

    def func(self):
        "Show color tables"

        if self.args.startswith("a"):
            # show ansi 16-color table
            from src.utils import ansi
            ap = ansi.ANSI_PARSER
            # ansi colors
            # show all ansi color-related codes
            col1 = ["%s%s{n" % (code, code.replace("{","{{")) for code, _ in ap.ext_ansi_map[:-1]]
            hi = "%ch"
            col2 = ["%s%s{n" % (code, code.replace("%", "%%")) for code, _ in ap.mux_ansi_map[3:-2]]
            col3 = ["%s%s{n" % (hi+code, (hi+code).replace("%", "%%")) for code, _ in ap.mux_ansi_map[3:-2]]
            table = utils.format_table([col1, col2, col3], extra_space=1)
            string = "ANSI colors:"
            for row in table:
                string += "\n" + "".join(row)
            #print string
            self.msg(string)
            self.msg("({{X and %%cx are black-on-black\n %%r - return, %%t - tab, %%b - space)")

        elif self.args.startswith("x"):
            # show xterm256 table
            table = [[],[],[],[],[],[],[],[],[],[],[],[]]
            for ir in range(6):
                for ig in range(6):
                    for ib in range(6):
                        # foreground table
                        table[ir].append("%%c%i%i%i%s{n" % (ir,ig,ib, "{{%i%i%i" % (ir,ig,ib)))
                        # background table
                        table[6+ir].append("%%cb%i%i%i%%c%i%i%i%s{n" % (ir,ig,ib,
                                                                        5-ir,5-ig,5-ib,
                                                                        "{{b%i%i%i" % (ir,ig,ib)))
            table = self.table_format(table)
            string = "Xterm256 colors (if not all hues show, your client might not report that it can handle xterm256):"
            for row in table:
                string += "\n" + "".join(row)
            self.msg(string)
            self.msg("(e.g. %%c123 and %%cb123 also work)")
        else:
            # malformed input
            self.msg("Usage: @color ansi|xterm256")

class CmdQuell(MuxPlayerCommand):
    """
    Quelling permissions

    Usage:
      quell
      unquell

    Normally the permission level of the Player is used when puppeting a
    Character/Object to determine access. This command will switch the lock
    system to make use of the puppeted Object's permissions instead. This is
    useful mainly for testing.
    Hierarchical permission quelling only work downwards, thus a Player cannot
    use a higher-permission Character to escalate their permission level.
    Use the unquell command to revert back to normal operation.
    """

    key = "@quell"
    aliases =["@unquell"]
    locks = "cmd:all()"
    help_category = "General"

    def _recache_locks(self, player):
        "Helper method to reset the lockhandler on an already puppeted object"
        if self.sessid:
            char = player.get_puppet(self.sessid)
            if char:
                # we are already puppeting an object. We need to reset the lock caches
                # (otherwise the superuser status change won't be visible until repuppet)
                char.locks.reset()

    def func(self):
        "Perform the command"
        player = self.caller
        permstr = player.is_superuser and " (superuser)" or " (%s)" % (", ".join(player.permissions))
        if self.cmdstring == '@unquell':
            if not player.get_attribute('_quell'):
                self.msg("Already using normal Player permissions%s." % permstr)
            else:
                player.del_attribute('_quell')
                self.msg("Player permissions%s restored." % permstr)
        else:
            if player.get_attribute('_quell'):
                self.msg("Already quelling Player%s permissions." % permstr)
                return
            player.set_attribute('_quell', True)
            self.msg("Quelling Player permissions%s. Use @unquell to get them back." % permstr)
        self._recache_locks(player)

