# Source code for evennia.contrib.base_systems.ingame_python.commands

    """
    Module containing the commands of the in-game Python system.
    """

    from datetime import datetime

    from django.conf import settings
    from evennia.utils.ansi import raw
    from evennia.utils.eveditor import EvEditor
    from evennia.utils.evtable import EvTable
    from evennia.utils.utils import class_from_module, time_format
    from evennia.contrib.base_systems.ingame_python.utils import get_event_handler

    COMMAND_DEFAULT_CLASS = class_from_module(settings.COMMAND_DEFAULT_CLASS)

    # Permissions
    WITH_VALIDATION = getattr(settings, "callbackS_WITH_VALIDATION", None)
    WITHOUT_VALIDATION = getattr(settings, "callbackS_WITHOUT_VALIDATION", "developer")
    VALIDATING = getattr(settings, "callbackS_VALIDATING", "developer")

    # Split help text
    BASIC_HELP = "Add, edit or delete callbacks."

    BASIC_USAGES = [
        "@call <object name> [= <callback name>]",
        "@call/add <object name> = <callback name> [parameters]",
        "@call/edit <object name> = <callback name> [callback number]",
        "@call/del <object name> = <callback name> [callback number]",
        "@call/tasks [object name [= <callback name>]]",
    ]

    BASIC_SWITCHES = [
        "add    - add and edit a new callback",
        "edit   - edit an existing callback",
        "del    - delete an existing callback",
        "tasks  - show the list of differed tasks",
    ]

    VALIDATOR_USAGES = ["@call/accept [object name = <callback name> [callback number]]"]

    VALIDATOR_SWITCHES = ["accept - show callbacks to be validated or accept one"]

    BASIC_TEXT = """
    This command is used to manipulate callbacks.  A callback can be linked to
    an object, to fire at a specific moment.  You can use the command without
    switches to see what callbacks are active on an object:
      @call self
    You can also specify a callback name if you want the list of callbacks
    associated with this object of this name:
      @call north = can_traverse
    You can also add a number after the callback name to see details on one callback:
      @call here = say 2
    You can also add, edit or remove callbacks using the add, edit or del switches.
    Additionally, you can see the list of differed tasks created by callbacks
    (chained events to be called) using the /tasks switch.
    """

    VALIDATOR_TEXT = """
    You can also use this command to validate callbacks.  Depending on your game
    setting, some users might be allowed to add new callbacks, but these callbacks
    will not be fired until you accept them.  To see the callbacks needing
    validation, enter the /accept switch without argument:
      @call/accept
    A table will show you the callbacks that are not validated yet, who created
    them and when.  You can then accept a specific callback:
      @call here = enter 1
    Use the /del switch to remove callbacks that should not be connected.
    """

    class CmdCallback(COMMAND_DEFAULT_CLASS):

        """
        Command to edit callbacks.
        """

        key = "@call"
        aliases = ["@callback", "@callbacks", "@calls"]
        locks = "cmd:perm({})".format(VALIDATING)
        if WITH_VALIDATION:
            locks += " or perm({})".format(WITH_VALIDATION)
        help_category = "Building"

        def get_help(self, caller, cmdset):
            """
            Return the help message for this command and this caller.

            The help text of this specific command will vary depending
            on user permission.

            Args:
                caller (Object or Account): the caller asking for help on the command.
                cmdset (CmdSet): the command set (if you need additional commands).

            Returns:
                docstring (str): the help text to provide the caller for this command.

            """
            lock = "perm({}) or perm(callbacks_validating)".format(VALIDATING)
            validator = caller.locks.check_lockstring(caller, lock)
            text = "\n" + BASIC_HELP + "\n\nUsages:\n  "

            # Usages
            text += "\n  ".join(BASIC_USAGES)
            if validator:
                text += "\n  " + "\n  ".join(VALIDATOR_USAGES)

            # Switches
            text += "\n\nSwitches:\n  "
            text += "\n  ".join(BASIC_SWITCHES)
            if validator:
                text += "\n  " + "\n  ".join(VALIDATOR_SWITCHES)

            # Text
            text += "\n" + BASIC_TEXT
            if validator:
                text += "\n" + VALIDATOR_TEXT

            return text

        def func(self):
            """Command body."""
            caller = self.caller
            lock = "perm({}) or perm(events_validating)".format(VALIDATING)
            validator = caller.locks.check_lockstring(caller, lock)
            lock = "perm({}) or perm(events_without_validation)".format(WITHOUT_VALIDATION)
            autovalid = caller.locks.check_lockstring(caller, lock)

            # First and foremost, get the callback handler and set other variables
            self.handler = get_event_handler()
            self.obj = None
            rhs = self.rhs or ""
            self.callback_name, sep, self.parameters = rhs.partition(" ")
            self.callback_name = self.callback_name.lower()
            self.is_validator = validator
            self.autovalid = autovalid
            if self.handler is None:
                caller.msg("The event handler is not running, can't " "access the event system.")
                return

            # Before the equal sign, there is an object name or nothing
            if self.lhs:
                self.obj = caller.search(self.lhs)
                if not self.obj:
                    return

            # Switches are mutually exclusive
            switch = self.switches and self.switches[0] or ""
            if switch in ("", "add", "edit", "del") and self.obj is None:
                caller.msg("Specify an object's name or #ID.")
                return

            if switch == "":
                self.list_callbacks()
            elif switch == "add":
                self.add_callback()
            elif switch == "edit":
                self.edit_callback()
            elif switch == "del":
                self.del_callback()
            elif switch == "accept" and validator:
                self.accept_callback()
            elif switch in ["tasks", "task"]:
                self.list_tasks()
            else:
                caller.msg("Mutually exclusive or invalid switches were " "used, cannot proceed.")

        def list_callbacks(self):
            """Display the list of callbacks connected to the object."""
            obj = self.obj
            callback_name = self.callback_name
            parameters = self.parameters
            callbacks = self.handler.get_callbacks(obj)
            types = self.handler.get_events(obj)

            if callback_name:
                # Check that the callback name can be found in this object
                created = callbacks.get(callback_name)
                if created is None:
                    self.msg("No callback {} has been set on {}.".format(callback_name, obj))
                    return

                if parameters:
                    # Check that the parameter points to an existing callback
                    try:
                        number = int(parameters) - 1
                        assert number >= 0
                        callback = callbacks[callback_name][number]
                    except (ValueError, AssertionError, IndexError):
                        self.msg(
                            "The callback {} {} cannot be found in {}.".format(
                                callback_name, parameters, obj
                            )
                        )
                        return

                    # Display the callback's details
                    author = callback.get("author")
                    author = author.key if author else "|gUnknown|n"
                    updated_by = callback.get("updated_by")
                    updated_by = updated_by.key if updated_by else "|gUnknown|n"
                    created_on = callback.get("created_on")
                    created_on = (
                        created_on.strftime("%Y-%m-%d %H:%M:%S") if created_on else "|gUnknown|n"
                    )
                    updated_on = callback.get("updated_on")
                    updated_on = (
                        updated_on.strftime("%Y-%m-%d %H:%M:%S") if updated_on else "|gUnknown|n"
                    )
                    msg = "Callback {} {} of {}:".format(callback_name, parameters, obj)
                    msg += "\nCreated by {} on {}.".format(author, created_on)
                    msg += "\nUpdated by {} on {}".format(updated_by, updated_on)

                    if self.is_validator:
                        if callback.get("valid"):
                            msg += "\nThis callback is |rconnected|n and active."
                        else:
                            msg += "\nThis callback |rhasn't been|n accepted yet."

                    msg += "\nCallback code:\n"
                    msg += raw(callback["code"])
                    self.msg(msg)
                    return

                # No parameter has been specified, display the table of callbacks
                cols = ["Number", "Author", "Updated", "Param"]
                if self.is_validator:
                    cols.append("Valid")

                table = EvTable(*cols, width=78)
                table.reformat_column(0, align="r")
                now = datetime.now()
                for i, callback in enumerate(created):
                    author = callback.get("author")
                    author = author.key if author else "|gUnknown|n"
                    updated_on = callback.get("updated_on")
                    if updated_on is None:
                        updated_on = callback.get("created_on")

                    if updated_on:
                        updated_on = "{} ago".format(
                            time_format((now - updated_on).total_seconds(), 4).capitalize()
                        )
                    else:
                        updated_on = "|gUnknown|n"
                    parameters = callback.get("parameters", "")

                    row = [str(i + 1), author, updated_on, parameters]
                    if self.is_validator:
                        row.append("Yes" if callback.get("valid") else "No")
                    table.add_row(*row)

                self.msg(str(table))
            else:
                names = list(set(list(types.keys()) + list(callbacks.keys())))
                table = EvTable("Callback name", "Number", "Description", valign="t", width=78)
                table.reformat_column(0, width=20)
                table.reformat_column(1, width=10, align="r")
                table.reformat_column(2, width=48)
                for name in sorted(names):
                    number = len(callbacks.get(name, []))
                    lines = sum(len(e["code"].splitlines()) for e in callbacks.get(name, []))
                    no = "{} ({})".format(number, lines)
                    description = types.get(name, (None, "Chained event."))[1]
                    description = description.strip("\n").splitlines()[0]
                    table.add_row(name, no, description)

                self.msg(str(table))

        def add_callback(self):
            """Add a callback."""
            obj = self.obj
            callback_name = self.callback_name
            types = self.handler.get_events(obj)

            # Check that the callback exists
            if not callback_name.startswith("chain_") and callback_name not in types:
                self.msg(
                    "The callback name {} can't be found in {} of "
                    "typeclass {}.".format(callback_name, obj, type(obj))
                )
                return

            definition = types.get(callback_name, (None, "Chained event."))
            description = definition[1]
            self.msg(raw(description.strip("\n")))

            # Open the editor
            callback = self.handler.add_callback(
                obj, callback_name, "", self.caller, False, parameters=self.parameters
            )

            # Lock this callback right away
            self.handler.db.locked.append((obj, callback_name, callback["number"]))

            # Open the editor for this callback
            self.caller.db._callback = callback
            EvEditor(
                self.caller,
                loadfunc=_ev_load,
                savefunc=_ev_save,
                quitfunc=_ev_quit,
                key="Callback {} of {}".format(callback_name, obj),
                persistent=True,
                codefunc=_ev_save,
            )

        def edit_callback(self):
            """Edit a callback."""
            obj = self.obj
            callback_name = self.callback_name
            parameters = self.parameters
            callbacks = self.handler.get_callbacks(obj)
            types = self.handler.get_events(obj)

            # If no callback name is specified, display the list of callbacks
            if not callback_name:
                self.list_callbacks()
                return

            # Check that the callback exists
            if callback_name not in callbacks:
                self.msg("The callback name {} can't be found in {}.".format(callback_name, obj))
                return

            # If there's only one callback, just edit it
            if len(callbacks[callback_name]) == 1:
                number = 0
                callback = callbacks[callback_name][0]
            else:
                if not parameters:
                    self.msg("Which callback do you wish to edit?  Specify a number.")
                    self.list_callbacks()
                    return

                # Check that the parameter points to an existing callback
                try:
                    number = int(parameters) - 1
                    assert number >= 0
                    callback = callbacks[callback_name][number]
                except (ValueError, AssertionError, IndexError):
                    self.msg(
                        "The callback {} {} cannot be found in {}.".format(
                            callback_name, parameters, obj
                        )
                    )
                    return

            # If caller can't edit without validation, forbid editing
            # others' works
            if not self.autovalid and callback["author"] is not self.caller:
                self.msg("You cannot edit this callback created by someone else.")
                return

            # If the callback is locked (edited by someone else)
            if (obj, callback_name, number) in self.handler.db.locked:
                self.msg("This callback is locked, you cannot edit it.")
                return

            self.handler.db.locked.append((obj, callback_name, number))

            # Check the definition of the callback
            definition = types.get(callback_name, (None, "Chained event."))
            description = definition[1]
            self.msg(raw(description.strip("\n")))

            # Open the editor
            callback = dict(callback)
            self.caller.db._callback = callback
            EvEditor(
                self.caller,
                loadfunc=_ev_load,
                savefunc=_ev_save,
                quitfunc=_ev_quit,
                key="Callback {} of {}".format(callback_name, obj),
                persistent=True,
                codefunc=_ev_save,
            )

        def del_callback(self):
            """Delete a callback."""
            obj = self.obj
            callback_name = self.callback_name
            parameters = self.parameters
            callbacks = self.handler.get_callbacks(obj)
            types = self.handler.get_events(obj)

            # If no callback name is specified, display the list of callbacks
            if not callback_name:
                self.list_callbacks()
                return

            # Check that the callback exists
            if callback_name not in callbacks:
                self.msg("The callback name {} can't be found in {}.".format(callback_name, obj))
                return

            # If there's only one callback, just delete it
            if len(callbacks[callback_name]) == 1:
                number = 0
                callback = callbacks[callback_name][0]
            else:
                if not parameters:
                    self.msg("Which callback do you wish to delete?  Specify " "a number.")
                    self.list_callbacks()
                    return

                # Check that the parameter points to an existing callback
                try:
                    number = int(parameters) - 1
                    assert number >= 0
                    callback = callbacks[callback_name][number]
                except (ValueError, AssertionError, IndexError):
                    self.msg(
                        "The callback {} {} cannot be found in {}.".format(
                            callback_name, parameters, obj
                        )
                    )
                    return

            # If caller can't edit without validation, forbid deleting
            # others' works
            if not self.autovalid and callback["author"] is not self.caller:
                self.msg("You cannot delete this callback created by someone else.")
                return

            # If the callback is locked (edited by someone else)
            if (obj, callback_name, number) in self.handler.db.locked:
                self.msg("This callback is locked, you cannot delete it.")
                return

            # Delete the callback
            self.handler.del_callback(obj, callback_name, number)
            self.msg("The callback {}[{}] of {} was deleted.".format(callback_name, number + 1, obj))

        def accept_callback(self):
            """Accept a callback."""
            obj = self.obj
            callback_name = self.callback_name
            parameters = self.parameters

            # If no object, display the list of callbacks to be checked
            if obj is None:
                table = EvTable("ID", "Type", "Object", "Name", "Updated by", "On", width=78)
                table.reformat_column(0, align="r")
                now = datetime.now()
                for obj, name, number in self.handler.db.to_valid:
                    callbacks = self.handler.get_callbacks(obj).get(name)
                    if callbacks is None:
                        continue

                    try:
                        callback = callbacks[number]
                    except IndexError:
                        continue

                    type_name = obj.typeclass_path.split(".")[-1]
                    by = callback.get("updated_by")
                    by = by.key if by else "|gUnknown|n"
                    updated_on = callback.get("updated_on")
                    if updated_on is None:
                        updated_on = callback.get("created_on")

                    if updated_on:
                        updated_on = "{} ago".format(
                            time_format((now - updated_on).total_seconds(), 4).capitalize()
                        )
                    else:
                        updated_on = "|gUnknown|n"

                    table.add_row(obj.id, type_name, obj, name, by, updated_on)
                self.msg(str(table))
                return

            # An object was specified
            callbacks = self.handler.get_callbacks(obj)
            types = self.handler.get_events(obj)

            # If no callback name is specified, display the list of callbacks
            if not callback_name:
                self.list_callbacks()
                return

            # Check that the callback exists
            if callback_name not in callbacks:
                self.msg("The callback name {} can't be found in {}.".format(callback_name, obj))
                return

            if not parameters:
                self.msg("Which callback do you wish to accept?  Specify a number.")
                self.list_callbacks()
                return

            # Check that the parameter points to an existing callback
            try:
                number = int(parameters) - 1
                assert number >= 0
                callback = callbacks[callback_name][number]
            except (ValueError, AssertionError, IndexError):
                self.msg(
                    "The callback {} {} cannot be found in {}.".format(callback_name, parameters, obj)
                )
                return

            # Accept the callback
            if callback["valid"]:
                self.msg("This callback has already been accepted.")
            else:
                self.handler.accept_callback(obj, callback_name, number)
                self.msg(
                    "The callback {} {} of {} has been accepted.".format(callback_name, parameters, obj)
                )

        def list_tasks(self):
            """List the active tasks."""
            obj = self.obj
            callback_name = self.callback_name
            handler = self.handler
            tasks = [(k, v[0], v[1], v[2]) for k, v in handler.db.tasks.items()]
            if obj:
                tasks = [task for task in tasks if task[2] is obj]
            if callback_name:
                tasks = [task for task in tasks if task[3] == callback_name]

            tasks.sort()
            table = EvTable("ID", "Object", "Callback", "In", width=78)
            table.reformat_column(0, align="r")
            now = datetime.now()
            for task_id, future, obj, callback_name in tasks:
                key = obj.get_display_name(self.caller)
                delta = time_format((future - now).total_seconds(), 1)
                table.add_row(task_id, key, callback_name, delta)

            self.msg(str(table))

    # Private functions to handle editing

    def _ev_load(caller):
        return caller.db._callback and caller.db._callback.get("code", "") or ""

    def _ev_save(caller, buf):
        """Save and add the callback."""
        lock = "perm({}) or perm(events_without_validation)".format(WITHOUT_VALIDATION)
        autovalid = caller.locks.check_lockstring(caller, lock)
        callback = caller.db._callback
        handler = get_event_handler()
        if (
            not handler
            or not callback
            or not all(key in callback for key in ("obj", "name", "number", "valid"))
        ):
            caller.msg("Couldn't save this callback.")
            return False

        if (callback["obj"], callback["name"], callback["number"]) in handler.db.locked:
            handler.db.locked.remove((callback["obj"], callback["name"], callback["number"]))

        handler.edit_callback(
            callback["obj"], callback["name"], callback["number"], buf, caller, valid=autovalid
        )
        return True

    def _ev_quit(caller):
        callback = caller.db._callback
        handler = get_event_handler()
        if (
            not handler
            or not callback
            or not all(key in callback for key in ("obj", "name", "number", "valid"))
        ):
            caller.msg("Couldn't save this callback.")
            return False

        if (callback["obj"], callback["name"], callback["number"]) in handler.db.locked:
            handler.db.locked.remove((callback["obj"], callback["name"], callback["number"]))

        del caller.db._callback
        caller.msg("Exited the code editor.")