"""
Module containing the building menu system.

Evennia contributor: vincent-lg 2018

Building menus are in-game menus, not unlike `EvMenu` though using a
different approach.  Building menus have been specifically designed to edit
information as a builder.  Creating a building menu in a command allows
builders quick-editing of a given object, like a room.  If you follow the
steps below to add the contrib, you will have access to an `@edit` command
that will edit any default object offering to change its key and description.

1. Import the `GenericBuildingCmd` class from this contrib in your `mygame/commands/default_cmdset.py` file:

    ```python
    from evennia.contrib.building_menu import GenericBuildingCmd
    ```

2. Below, add the command in the `CharacterCmdSet`:

    ```python
    # ... These lines should exist in the file
    class CharacterCmdSet(default_cmds.CharacterCmdSet):
        key = "DefaultCharacter"

        def at_cmdset_creation(self):
            super(CharacterCmdSet, self).at_cmdset_creation()
            # ... add the line below
            self.add(GenericBuildingCmd())
    ```

The `@edit` command will allow you to edit any object.  You will need to
specify the object name or ID as an argument.  For instance: `@edit here`
will edit the current room.  However, building menus can perform much more
than this very simple example, read on for more details.

Building menus can be set to edit about anything.  Here is an example of
output you could obtain when editing the room:

```
 Editing the room: Limbo(#2)

 [T]itle: the limbo room
 [D]escription
    This is the limbo room.  You can easily change this default description,
    either by using the |y@desc/edit|n command, or simply by entering this
    menu (enter |yd|n).
 [E]xits:
     north to A parking(#4)
 [Q]uit this menu
```

From there, you can open the title choice by pressing t.  You can then
change the room title by simply entering text, and go back to the
main menu entering @ (all this is customizable).  Press q to quit this menu.

The first thing to do is to create a new module and place a class
inheriting from `BuildingMenu` in it.

```python
from evennia.contrib.building_menu import BuildingMenu

class RoomBuildingMenu(BuildingMenu):
    # ...
```

Next, override the `init` method.  You can add choices (like the title,
description, and exits choices as seen above) by using the `add_choice`
method.

```
class RoomBuildingMenu(BuildingMenu):
    def init(self, room):
        self.add_choice("title", "t", attr="key")
```

That will create the first choice, the title choice.  If one opens your menu
and enter t, she will be in the title choice.  She can change the title
(it will write in the room's `key` attribute) and then go back to the
main menu using `@`.

`add_choice` has a lot of arguments and offers a great deal of
flexibility.  The most useful ones is probably the usage of callbacks,
as you can set almost any argument in `add_choice` to be a callback, a
function that you have defined above in your module.  This function will be
called when the menu element is triggered.

Notice that in order to edit a description, the best method to call isn't
`add_choice`, but `add_choice_edit`.  This is a convenient shortcut
which is available to quickly open an `EvEditor` when entering this choice
and going back to the menu when the editor closes.

```
class RoomBuildingMenu(BuildingMenu):
    def init(self, room):
        self.add_choice("title", "t", attr="key")
        self.add_choice_edit("description", key="d", attr="db.desc")
```

When you wish to create a building menu, you just need to import your
class, create it specifying your intended caller and object to edit,
then call `open`:

```python
from <wherever> import RoomBuildingMenu

class CmdEdit(Command):

    key = "redit"

    def func(self):
        menu = RoomBuildingMenu(self.caller, self.caller.location)
        menu.open()
```

This is a very short introduction.  For more details, see the online tutorial
(https://github.com/evennia/evennia/wiki/Building-menus) or read the
heavily-documented code below.

"""

from inspect import getargspec
from textwrap import dedent

from django.conf import settings
from evennia import Command, CmdSet
from evennia.commands import cmdhandler
from evennia.utils.ansi import strip_ansi
from evennia.utils.eveditor import EvEditor
from evennia.utils.logger import log_err, log_trace
from evennia.utils.utils import class_from_module


# Constants
_MAX_TEXT_WIDTH = settings.CLIENT_DEFAULT_WIDTH
_CMD_NOMATCH = cmdhandler.CMD_NOMATCH
_CMD_NOINPUT = cmdhandler.CMD_NOINPUT


# Private functions
def _menu_loadfunc(caller):
    obj, attr = caller.attributes.get("_building_menu_to_edit", [None, None])
    if obj and attr:
        for part in attr.split(".")[:-1]:
            obj = getattr(obj, part)

    return getattr(obj, attr.split(".")[-1]) if obj is not None else ""


def _menu_savefunc(caller, buf):
    obj, attr = caller.attributes.get("_building_menu_to_edit", [None, None])
    if obj and attr:
        for part in attr.split(".")[:-1]:
            obj = getattr(obj, part)

        setattr(obj, attr.split(".")[-1], buf)

    caller.attributes.remove("_building_menu_to_edit")
    return True


def _menu_quitfunc(caller):
    caller.cmdset.add(
        BuildingMenuCmdSet,
        permanent=caller.ndb._building_menu and caller.ndb._building_menu.persistent or False,
    )
    if caller.ndb._building_menu:
        caller.ndb._building_menu.move(back=True)


def _call_or_get(value, menu=None, choice=None, string=None, obj=None, caller=None):
    """
    Call the value, if appropriate, or just return it.

    Args:
        value (any): the value to obtain.  It might be a callable (see note).

    Keyword Args:
        menu (BuildingMenu, optional): the building menu to pass to value
                if it is a callable.
        choice (Choice, optional): the choice to pass to value if a callable.
        string (str, optional): the raw string to pass to value if a callback.
        obj (Object): the object to pass to value if a callable.
        caller (Account or Object, optional): the caller to pass to value
                if a callable.

    Returns:
        The value itself.  If the argument is a function, call it with
        specific arguments (see note).

    Note:
        If `value` is a function, call it with varying arguments.  The
        list of arguments will depend on the argument names in your callable.
        - An argument named `menu` will contain the building menu or None.
        - The `choice` argument will contain the choice or None.
        - The `string` argument will contain the raw string or None.
        - The `obj` argument will contain the object or None.
        - The `caller` argument will contain the caller or None.
        - Any other argument will contain the object (`obj`).
        Thus, you could define callbacks like this:
            def on_enter(menu, caller, obj):
            def on_nomatch(string, choice, menu):
            def on_leave(caller, room): # note that room will contain `obj`

    """
    if callable(value):
        # Check the function arguments
        kwargs = {}
        spec = getargspec(value)
        args = spec.args
        if spec.keywords:
            kwargs.update(dict(menu=menu, choice=choice, string=string, obj=obj, caller=caller))
        else:
            if "menu" in args:
                kwargs["menu"] = menu
            if "choice" in args:
                kwargs["choice"] = choice
            if "string" in args:
                kwargs["string"] = string
            if "obj" in args:
                kwargs["obj"] = obj
            if "caller" in args:
                kwargs["caller"] = caller

        # Fill missing arguments
        for arg in args:
            if arg not in kwargs:
                kwargs[arg] = obj

        # Call the function and return its return value
        return value(**kwargs)

    return value


# Helper functions, to be used in menu choices


def menu_setattr(menu, choice, obj, string):
    """
    Set the value at the specified attribute.

    Args:
        menu (BuildingMenu): the menu object.
        choice (Chocie): the specific choice.
        obj (Object): the object to modify.
        string (str): the string with the new value.

    Note:
        This function is supposed to be used as a default to
        `BuildingMenu.add_choice`, when an attribute name is specified
        (in the `attr` argument) but no function `on_nomatch` is defined.

    """
    attr = getattr(choice, "attr", None) if choice else None
    if choice is None or string is None or attr is None or menu is None:
        log_err(
            dedent(
                """
                The `menu_setattr` function was called to set the attribute {} of object {} to {},
                but the choice {} of menu {} or another information is missing.
            """.format(
                    attr, obj, repr(string), choice, menu
                )
            ).strip("\n")
        ).strip()
        return

    for part in attr.split(".")[:-1]:
        obj = getattr(obj, part)

    setattr(obj, attr.split(".")[-1], string)
    return True


def menu_quit(caller, menu):
    """
    Quit the menu, closing the CmdSet.

    Args:
        caller (Account or Object): the caller.
        menu (BuildingMenu): the building menu to close.

    Note:
        This callback is used by default when using the
        `BuildingMenu.add_choice_quit` method.  This method is called
        automatically if the menu has no parent.

    """
    if caller is None or menu is None:
        log_err(
            "The function `menu_quit` was called with missing "
            "arguments: caller={}, menu={}".format(caller, menu)
        )

    if caller.cmdset.has(BuildingMenuCmdSet):
        menu.close()
        caller.msg("Closing the building menu.")
    else:
        caller.msg("It looks like the building menu has already been closed.")


def menu_edit(caller, choice, obj):
    """
    Open the EvEditor to edit a specified attribute.

    Args:
        caller (Account or Object): the caller.
        choice (Choice): the choice object.
        obj (Object): the object to edit.

    """
    attr = choice.attr
    caller.db._building_menu_to_edit = (obj, attr)
    caller.cmdset.remove(BuildingMenuCmdSet)
    EvEditor(
        caller,
        loadfunc=_menu_loadfunc,
        savefunc=_menu_savefunc,
        quitfunc=_menu_quitfunc,
        key="editor",
        persistent=True,
    )


# Building menu commands and CmdSet


class CmdNoInput(Command):

    """No input has been found."""

    key = _CMD_NOINPUT
    locks = "cmd:all()"

    def __init__(self, **kwargs):
        self.menu = kwargs.pop("building_menu", None)
        super(Command, self).__init__(**kwargs)

    def func(self):
        """Display the menu or choice text."""
        if self.menu:
            self.menu.display()
        else:
            log_err("When CMDNOINPUT was called, the building menu couldn't be found")
            self.caller.msg("|rThe building menu couldn't be found, remove the CmdSet.|n")
            self.caller.cmdset.delete(BuildingMenuCmdSet)


class CmdNoMatch(Command):

    """No input has been found."""

    key = _CMD_NOMATCH
    locks = "cmd:all()"

    def __init__(self, **kwargs):
        self.menu = kwargs.pop("building_menu", None)
        super(Command, self).__init__(**kwargs)

    def func(self):
        """Call the proper menu or redirect to nomatch."""
        raw_string = self.args.rstrip()
        if self.menu is None:
            log_err("When CMDNOMATCH was called, the building menu couldn't be found")
            self.caller.msg("|rThe building menu couldn't be found, remove the CmdSet.|n")
            self.caller.cmdset.delete(BuildingMenuCmdSet)
            return

        choice = self.menu.current_choice
        if raw_string in self.menu.keys_go_back:
            if self.menu.keys:
                self.menu.move(back=True)
            elif self.menu.parents:
                self.menu.open_parent_menu()
            else:
                self.menu.display()
        elif choice:
            if choice.nomatch(raw_string):
                self.caller.msg(choice.format_text())
        else:
            for choice in self.menu.relevant_choices:
                if choice.key.lower() == raw_string.lower() or any(
                    raw_string.lower() == alias for alias in choice.aliases
                ):
                    self.menu.move(choice.key)
                    return

            self.msg("|rUnknown command: {}|n.".format(raw_string))


class BuildingMenuCmdSet(CmdSet):

    """Building menu CmdSet."""

    key = "building_menu"
    priority = 5

    def at_cmdset_creation(self):
        """Populates the cmdset with commands."""
        caller = self.cmdsetobj

        # The caller could recall the menu
        menu = caller.ndb._building_menu
        if menu is None:
            menu = caller.db._building_menu
            if menu:
                menu = BuildingMenu.restore(caller)

        cmds = [CmdNoInput, CmdNoMatch]
        for cmd in cmds:
            self.add(cmd(building_menu=menu))


# Menu classes


class Choice(object):

    """A choice object, created by `add_choice`."""

    def __init__(
        self,
        title,
        key=None,
        aliases=None,
        attr=None,
        text=None,
        glance=None,
        on_enter=None,
        on_nomatch=None,
        on_leave=None,
        menu=None,
        caller=None,
        obj=None,
    ):
        """Constructor.

        Args:
            title (str): the choice's title.
            key (str, optional): the key of the letters to type to access
                    the choice.  If not set, try to guess it based on the title.
            aliases (list of str, optional): the allowed aliases for this choice.
            attr (str, optional): the name of the attribute of 'obj' to set.
            text (str or callable, optional): a text to be displayed for this
                    choice.  It can be a callable.
            glance (str or callable, optional): an at-a-glance summary of the
                    sub-menu shown in the main menu.  It can be set to
                    display the current value of the attribute in the
                    main menu itself.
            menu (BuildingMenu, optional): the parent building menu.
            on_enter (callable, optional): a callable to call when the
                    caller enters into the choice.
            on_nomatch (callable, optional): a callable to call when no
                    match is entered in the choice.
            on_leave (callable, optional): a callable to call when the caller
                    leaves the choice.
            caller (Account or Object, optional): the caller.
            obj (Object, optional): the object to edit.

        """
        self.title = title
        self.key = key
        self.aliases = aliases
        self.attr = attr
        self.text = text
        self.glance = glance
        self.on_enter = on_enter
        self.on_nomatch = on_nomatch
        self.on_leave = on_leave
        self.menu = menu
        self.caller = caller
        self.obj = obj

    def __repr__(self):
        return "<Choice (title={}, key={})>".format(self.title, self.key)

    @property
    def keys(self):
        """Return a tuple of keys separated by `sep_keys`."""
        return tuple(self.key.split(self.menu.sep_keys))

    def format_text(self):
        """Format the choice text and return it, or an empty string."""
        text = ""
        if self.text:
            text = _call_or_get(
                self.text, menu=self.menu, choice=self, string="", caller=self.caller, obj=self.obj
            )
            text = dedent(text.strip("\n"))
            text = text.format(obj=self.obj, caller=self.caller)

        return text

    def enter(self, string):
        """Called when the user opens the choice.

        Args:
            string (str): the entered string.

        """
        if self.on_enter:
            _call_or_get(
                self.on_enter,
                menu=self.menu,
                choice=self,
                string=string,
                caller=self.caller,
                obj=self.obj,
            )

    def nomatch(self, string):
        """Called when the user entered something in the choice.

        Args:
            string (str): the entered string.

        Returns:
            to_display (bool): The return value of `nomatch` if set or
            `True`.  The rule is that if `no_match` returns `True`,
            then the choice or menu is displayed.

        """
        if self.on_nomatch:
            return _call_or_get(
                self.on_nomatch,
                menu=self.menu,
                choice=self,
                string=string,
                caller=self.caller,
                obj=self.obj,
            )

        return True

    def leave(self, string):
        """Called when the user closes the choice.

        Args:
            string (str): the entered string.

        """
        if self.on_leave:
            _call_or_get(
                self.on_leave,
                menu=self.menu,
                choice=self,
                string=string,
                caller=self.caller,
                obj=self.obj,
            )


class BuildingMenu(object):

    """
    Class allowing to create and set building menus to edit specific objects.

    A building menu is somewhat similar to `EvMenu`, but designed to edit
    objects by builders, although it can be used for players in some contexts.
    You could, for instance, create a building menu to edit a room with a
    sub-menu for the room's key, another for the room's description,
    another for the room's exits, and so on.

    To add choices (simple sub-menus), you should call `add_choice` (see the
    full documentation of this method).  With most arguments, you can
    specify either a plain string or a callback.  This callback will be
    called when the operation is to be performed.

    Some methods are provided for frequent needs (see the `add_choice_*`
    methods).  Some helper functions are defined at the top of this
    module in order to be used as arguments to `add_choice`
    in frequent cases.

    """

    keys_go_back = ["@"]  # The keys allowing to go back in the menu tree
    sep_keys = "."  # The key separator for menus with more than 2 levels
    joker_key = "*"  # The special key meaning "anything" in a choice key
    min_shortcut = 1  # The minimum length of shortcuts when `key` is not set

    def __init__(
        self,
        caller=None,
        obj=None,
        title="Building menu: {obj}",
        keys=None,
        parents=None,
        persistent=False,
    ):
        """Constructor, you shouldn't override.  See `init` instead.

        Args:
            caller (Account or Object): the caller.
            obj (Object): the object to be edited, like a room.
            title (str, optional): the menu title.
            keys (list of str, optional): the starting menu keys (None
                    to start from the first level).
            parents (tuple, optional): information for parent menus,
                    automatically supplied.
            persistent (bool, optional): should this building menu
                    survive a reload/restart?

        Note:
            If some of these options have to be changed, it is
            preferable to do so in the `init` method and not to
            override `__init__`.  For instance:
                class RoomBuildingMenu(BuildingMenu):
                    def init(self, room):
                        self.title = "Menu for room: {obj.key}(#{obj.id})"
                        # ...

        """
        self.caller = caller
        self.obj = obj
        self.title = title
        self.keys = keys or []
        self.parents = parents or ()
        self.persistent = persistent
        self.choices = []
        self.cmds = {}
        self.can_quit = False

        if obj:
            self.init(obj)
            if not parents and not self.can_quit:
                # Automatically add the menu to quit
                self.add_choice_quit(key=None)
            self._add_keys_choice()

    @property
    def current_choice(self):
        """Return the current choice or None.

        Returns:
            choice (Choice): the current choice or None.

        Note:
            We use the menu keys to identify the current position of
            the caller in the menu.  The menu `keys` hold a list of
            keys that should match a choice to be usable.

        """
        menu_keys = self.keys
        if not menu_keys:
            return None

        for choice in self.choices:
            choice_keys = choice.keys
            if len(menu_keys) == len(choice_keys):
                # Check all the intermediate keys
                common = True
                for menu_key, choice_key in zip(menu_keys, choice_keys):
                    if choice_key == self.joker_key:
                        continue

                    if not isinstance(menu_key, str) or menu_key != choice_key:
                        common = False
                        break

                if common:
                    return choice

        return None

    @property
    def relevant_choices(self):
        """Only return the relevant choices according to the current meny key.

        Returns:
            relevant (list of Choice object): the relevant choices.

        Note:
            We use the menu keys to identify the current position of
            the caller in the menu.  The menu `keys` hold a list of
            keys that should match a choice to be usable.

        """
        menu_keys = self.keys
        relevant = []
        for choice in self.choices:
            choice_keys = choice.keys
            if not menu_keys and len(choice_keys) == 1:
                # First level choice with the menu key empty, that's relevant
                relevant.append(choice)
            elif len(menu_keys) == len(choice_keys) - 1:
                # Check all the intermediate keys
                common = True
                for menu_key, choice_key in zip(menu_keys, choice_keys):
                    if choice_key == self.joker_key:
                        continue

                    if not isinstance(menu_key, str) or menu_key != choice_key:
                        common = False
                        break

                if common:
                    relevant.append(choice)

        return relevant

    def _save(self):
        """Save the menu in a attributes on the caller.

        If `persistent` is set to `True`, also save in a persistent attribute.

        """
        self.caller.ndb._building_menu = self

        if self.persistent:
            self.caller.db._building_menu = {
                "class": type(self).__module__ + "." + type(self).__name__,
                "obj": self.obj,
                "title": self.title,
                "keys": self.keys,
                "parents": self.parents,
                "persistent": self.persistent,
            }

    def _add_keys_choice(self):
        """Add the choices' keys if some choices don't have valid keys."""
        # If choices have been added without keys, try to guess them
        for choice in self.choices:
            if not choice.key:
                title = strip_ansi(choice.title.strip()).lower()
                length = self.min_shortcut
                while length <= len(title):
                    i = 0
                    while i < len(title) - length + 1:
                        guess = title[i : i + length]
                        if guess not in self.cmds:
                            choice.key = guess
                            break

                        i += 1

                    if choice.key:
                        break

                    length += 1

                if choice.key:
                    self.cmds[choice.key] = choice
                else:
                    raise ValueError("Cannot guess the key for {}".format(choice))

    def init(self, obj):
        """Create the sub-menu to edit the specified object.

        Args:
            obj (Object): the object to edit.

        Note:
            This method is probably to be overridden in your subclasses.
            Use `add_choice` and its variants to create menu choices.

        """
        pass

    def add_choice(
        self,
        title,
        key=None,
        aliases=None,
        attr=None,
        text=None,
        glance=None,
        on_enter=None,
        on_nomatch=None,
        on_leave=None,
    ):
        """
        Add a choice, a valid sub-menu, in the current builder menu.

        Args:
            title (str): the choice's title.
            key (str, optional): the key of the letters to type to access
                    the sub-neu.  If not set, try to guess it based on the
                    choice title.
            aliases (list of str, optional): the aliases for this choice.
            attr (str, optional): the name of the attribute of 'obj' to set.
                    This is really useful if you want to edit an
                    attribute of the object (that's a frequent need).  If
                    you don't want to do so, just use the `on_*` arguments.
            text (str or callable, optional): a text to be displayed when
                    the menu is opened  It can be a callable.
            glance (str or callable, optional): an at-a-glance summary of the
                    sub-menu shown in the main menu.  It can be set to
                    display the current value of the attribute in the
                    main menu itself.
            on_enter (callable, optional): a callable to call when the
                    caller enters into this choice.
            on_nomatch (callable, optional): a callable to call when
                    the caller enters something in this choice.  If you
                    don't set this argument but you have specified
                    `attr`, then `obj`.`attr` will be set with the value
                    entered by the user.
            on_leave (callable, optional): a callable to call when the
                    caller leaves the choice.

        Returns:
            choice (Choice): the newly-created choice.

        Raises:
            ValueError if the choice cannot be added.

        Note:
            Most arguments can be callables, like functions.  This has the
            advantage of allowing great flexibility.  If you specify
            a callable in most of the arguments, the callable should return
            the value expected by the argument (a str more often than
            not).  For instance, you could set a function to be called
            to get the menu text, which allows for some filtering:
                def text_exits(menu):
                    return "Some text to display"
                class RoomBuildingMenu(BuildingMenu):
                    def init(self):
                        self.add_choice("exits", key="x", text=text_exits)

            The allowed arguments in a callable are specific to the
            argument names (they are not sensitive to orders, not all
            arguments have to be present).  For more information, see
            `_call_or_get`.

        """
        key = key or ""
        key = key.lower()
        aliases = aliases or []
        aliases = [a.lower() for a in aliases]
        if attr and on_nomatch is None:
            on_nomatch = menu_setattr

        if key and key in self.cmds:
            raise ValueError(
                "A conflict exists between {} and {}, both use "
                "key or alias {}".format(self.cmds[key], title, repr(key))
            )

        if attr:
            if glance is None:
                glance = "{obj." + attr + "}"
            if text is None:
                text = """
                        -------------------------------------------------------------------------------
                        {attr} for {{obj}}(#{{obj.id}})

                        You can change this value simply by entering it.
                        Use |y{back}|n to go back to the main menu.

                        Current value: |c{{{obj_attr}}}|n
                """.format(
                    attr=attr, obj_attr="obj." + attr, back="|n or |y".join(self.keys_go_back)
                )

        choice = Choice(
            title,
            key=key,
            aliases=aliases,
            attr=attr,
            text=text,
            glance=glance,
            on_enter=on_enter,
            on_nomatch=on_nomatch,
            on_leave=on_leave,
            menu=self,
            caller=self.caller,
            obj=self.obj,
        )
        self.choices.append(choice)
        if key:
            self.cmds[key] = choice

        for alias in aliases:
            self.cmds[alias] = choice

        return choice

    def add_choice_edit(
        self,
        title="description",
        key="d",
        aliases=None,
        attr="db.desc",
        glance="\n   {obj.db.desc}",
        on_enter=None,
    ):
        """
        Add a simple choice to edit a given attribute in the EvEditor.

        Args:
            title (str, optional): the choice's title.
            key (str, optional): the choice's key.
            aliases (list of str, optional): the choice's aliases.
            glance (str or callable, optional): the at-a-glance description.
            on_enter (callable, optional): a different callable to edit
                    the attribute.

        Returns:
            choice (Choice): the newly-created choice.

        Note:
            This is just a shortcut method, calling `add_choice`.
            If `on_enter` is not set, use `menu_edit` which opens
            an EvEditor to edit the specified attribute.
            When the caller closes the editor (with :q), the menu
            will be re-opened.

        """
        on_enter = on_enter or menu_edit
        return self.add_choice(
            title, key=key, aliases=aliases, attr=attr, glance=glance, on_enter=on_enter, text=""
        )

    def add_choice_quit(self, title="quit the menu", key="q", aliases=None, on_enter=None):
        """
        Add a simple choice just to quit the building menu.

        Args:
            title (str, optional): the choice's title.
            key (str, optional): the choice's key.
            aliases (list of str, optional): the choice's aliases.
            on_enter (callable, optional): a different callable
                    to quit the building menu.

        Note:
            This is just a shortcut method, calling `add_choice`.
            If `on_enter` is not set, use `menu_quit` which simply
            closes the menu and displays a message.  It also
            removes the CmdSet from the caller.  If you supply
            another callable instead, make sure to do the same.

        """
        on_enter = on_enter or menu_quit
        self.can_quit = True
        return self.add_choice(title, key=key, aliases=aliases, on_enter=on_enter)

    def open(self):
        """Open the building menu for the caller.

        Note:
            This method should be called once when the building menu
            has been instanciated.  From there, the building menu will
            be re-created automatically when the server
            reloads/restarts, assuming `persistent` is set to `True`.

        """
        caller = self.caller
        self._save()

        # Remove the same-key cmdset if exists
        if caller.cmdset.has(BuildingMenuCmdSet):
            caller.cmdset.remove(BuildingMenuCmdSet)

        self.caller.cmdset.add(BuildingMenuCmdSet, permanent=self.persistent)
        self.display()

    def open_parent_menu(self):
        """Open the parent menu, using `self.parents`.

        Note:
            You probably don't need to call this method directly,
            since the caller can go back to the parent menu using the
            `keys_go_back` automatically.

        """
        parents = list(self.parents)
        if parents:
            parent_class, parent_obj, parent_keys = parents[-1]
            del parents[-1]

            if self.caller.cmdset.has(BuildingMenuCmdSet):
                self.caller.cmdset.remove(BuildingMenuCmdSet)

            try:
                menu_class = class_from_module(parent_class)
            except Exception:
                log_trace(
                    "BuildingMenu: attempting to load class {} failed".format(repr(parent_class))
                )
                return

            # Create the parent menu
            try:
                building_menu = menu_class(
                    self.caller, parent_obj, keys=parent_keys, parents=tuple(parents)
                )
            except Exception:
                log_trace(
                    "An error occurred while creating building menu {}".format(repr(parent_class))
                )
                return
            else:
                return building_menu.open()

    def open_submenu(self, submenu_class, submenu_obj, parent_keys=None):
        """
        Open a sub-menu, closing the current menu and opening the new one.

        Args:
            submenu_class (str): the submenu class as a Python path.
            submenu_obj (Object): the object to give to the submenu.
            parent_keys (list of str, optional): the parent keys when
                    the submenu is closed.

        Note:
            When the user enters `@` in the submenu, she will go back to
            the current menu, with the `parent_keys` set as its keys.
            Therefore, you should set it on the keys of the choice that
            should be opened when the user leaves the submenu.

        Returns:
            new_menu (BuildingMenu): the new building menu or None.

        """
        parent_keys = parent_keys or []
        parents = list(self.parents)
        parents.append((type(self).__module__ + "." + type(self).__name__, self.obj, parent_keys))
        if self.caller.cmdset.has(BuildingMenuCmdSet):
            self.caller.cmdset.remove(BuildingMenuCmdSet)

        # Shift to the new menu
        try:
            menu_class = class_from_module(submenu_class)
        except Exception:
            log_trace(
                "BuildingMenu: attempting to load class {} failed".format(repr(submenu_class))
            )
            return

        # Create the submenu
        try:
            building_menu = menu_class(self.caller, submenu_obj, parents=parents)
        except Exception:
            log_trace(
                "An error occurred while creating building menu {}".format(repr(submenu_class))
            )
            return
        else:
            return building_menu.open()

    def move(self, key=None, back=False, quiet=False, string=""):
        """
        Move inside the menu.

        Args:
            key (any): the portion of the key to add to the current
                    menu keys.  If you wish to go back in the menu
                    tree, don't provide a `key`, just set `back` to `True`.
            back (bool, optional): go back in the menu (`False` by default).
            quiet (bool, optional): should the menu or choice be
                    displayed afterward?
            string (str, optional): the string sent by the caller to move.

        Note:
            This method will need to be called directly should you
            use more than two levels in your menu.  For instance,
            in your room menu, if you want to have an "exits"
            option, and then be able to enter "north" in this
            choice to edit an exit.  The specific exit choice
            could be a different menu (with a different class), but
            it could also be an additional level in your original menu.
            If that's the case, you will need to use this method.

        """
        choice = self.current_choice
        if choice:
            choice.leave("")

        if not back:  # Move forward
            if not key:
                raise ValueError("you are asking to move forward, you should specify a key.")

            self.keys.append(key)
        else:  # Move backward
            if not self.keys:
                raise ValueError(
                    "you already are at the top of the tree, you cannot move backward."
                )

            del self.keys[-1]

        self._save()
        choice = self.current_choice
        if choice:
            choice.enter(string)

        if not quiet:
            self.display()

    def close(self):
        """Close the building menu, removing the CmdSet."""
        if self.caller.cmdset.has(BuildingMenuCmdSet):
            self.caller.cmdset.delete(BuildingMenuCmdSet)
        if self.caller.attributes.has("_building_menu"):
            self.caller.attributes.remove("_building_menu")
        if self.caller.nattributes.has("_building_menu"):
            self.caller.nattributes.remove("_building_menu")

    # Display methods.  Override for customization
    def display_title(self):
        """Return the menu title to be displayed."""
        return _call_or_get(self.title, menu=self, obj=self.obj, caller=self.caller).format(
            obj=self.obj
        )

    def display_choice(self, choice):
        """Display the specified choice.

        Args:
            choice (Choice): the menu choice.

        """
        title = _call_or_get(
            choice.title, menu=self, choice=choice, obj=self.obj, caller=self.caller
        )
        clear_title = title.lower()
        pos = clear_title.find(choice.key.lower())
        ret = " "
        if pos >= 0:
            ret += title[:pos] + "[|y" + choice.key.title() + "|n]" + title[pos + len(choice.key) :]
        else:
            ret += "[|y" + choice.key.title() + "|n] " + title

        if choice.glance:
            glance = _call_or_get(
                choice.glance, menu=self, choice=choice, caller=self.caller, string="", obj=self.obj
            )
            glance = glance.format(obj=self.obj, caller=self.caller)

            ret += ": " + glance

        return ret

    def display(self):
        """Display the entire menu or a single choice, depending on the keys."""
        choice = self.current_choice
        if self.keys and choice:
            text = choice.format_text()
        else:
            text = self.display_title() + "\n"
            for choice in self.relevant_choices:
                text += "\n" + self.display_choice(choice)

        self.caller.msg(text)

    @staticmethod
    def restore(caller):
        """Restore the building menu for the caller.

        Args:
            caller (Account or Object): the caller.

        Note:
            This method should be automatically called if a menu is
            saved in the caller, but the object itself cannot be found.

        """
        menu = caller.db._building_menu
        if menu:
            class_name = menu.get("class")
            if not class_name:
                log_err(
                    "BuildingMenu: on caller {}, a persistent attribute holds building menu "
                    "data, but no class could be found to restore the menu".format(caller)
                )
                return

            try:
                menu_class = class_from_module(class_name)
            except Exception:
                log_trace(
                    "BuildingMenu: attempting to load class {} failed".format(repr(class_name))
                )
                return

            # Create the menu
            obj = menu.get("obj")
            keys = menu.get("keys")
            title = menu.get("title", "")
            parents = menu.get("parents")
            persistent = menu.get("persistent", False)
            try:
                building_menu = menu_class(
                    caller, obj, title=title, keys=keys, parents=parents, persistent=persistent
                )
            except Exception:
                log_trace(
                    "An error occurred while creating building menu {}".format(repr(class_name))
                )
                return

            return building_menu


# Generic building menu and command
class GenericBuildingMenu(BuildingMenu):

    """A generic building menu, allowing to edit any object.

    This is more a demonstration menu.  By default, it allows to edit the
    object key and description.  Nevertheless, it will be useful to demonstrate
    how building menus are meant to be used.

    """

    def init(self, obj):
        """Build the meny, adding the 'key' and 'description' choices.

        Args:
            obj (Object): any object to be edited, like a character or room.

        Note:
            The 'quit' choice will be automatically added, though you can
            call `add_choice_quit` to add this choice with different options.

        """
        self.add_choice(
            "key",
            key="k",
            attr="key",
            glance="{obj.key}",
            text="""
                -------------------------------------------------------------------------------
                Editing the key of {{obj.key}}(#{{obj.id}})

                You can change the simply by entering it.
                Use |y{back}|n to go back to the main menu.

                Current key: |c{{obj.key}}|n
        """.format(
                back="|n or |y".join(self.keys_go_back)
            ),
        )
        self.add_choice_edit("description", key="d", attr="db.desc")


class GenericBuildingCmd(Command):

    """
    Generic building command.

    Syntax:
      @edit [object]

    Open a building menu to edit the specified object.  This menu allows to
    change the object's key and description.

    Examples:
      @edit here
      @edit self
      @edit #142

    """

    key = "@edit"

    def func(self):
        if not self.args.strip():
            self.msg("You should provide an argument to this function: the object to edit.")
            return

        obj = self.caller.search(self.args.strip(), global_search=True)
        if not obj:
            return

        menu = GenericBuildingMenu(self.caller, obj)
        menu.open()
