"""
.. moduleauthor:: Paul Wiemann <paulwiemann@gmail.com>
"""

from . import alfredlog
from ._helper import _DictObj, check_name
from .exceptions import AlfredError


class ExpMember:
    """
    Baseclass for sections and pages.

    Args:
        name (str): Name of the member. Must be unique throughout the
            experiment. Can be defined as a class attribute. When you
            write a page or section in *class style*, the name can be
            inferred from the class name and does not need to be
            defined explicitly again. If a name is defined explicitly
            as a class attribute, that name is used.
        title (str): Title of the member. Can be defined as a class
            attribute.
        subtitle (str): Subtitle of the member. Can be defined as a
            class attribute.
        vargs (dict): A dictionary that can be used to pass additional
            arguments to the page. The arguments are then available as
            an instance attribute. As a special feature, the instance
            attribute allows you to access the values of the dictionary
            not only via the usual square-bracket notation, but also
            via dot-notation.

            This argument fulfills a similar function as ``**kwargs`` do
            sometimes, but it makes sure that user-defined additional
            arguments will not collide with inherited keywords arguments.

            Can be defined as a class attribute.
    """

    #: Unique name of the member.
    name: str = None

    #: If *True*, the member will spawn a logger that can be configured
    #: individually for each instance
    instance_log: bool = False

    #: Name of the parent section. Used when a member is appended to
    #: the :class:`.Experiment`. If *None*, a member will be appended
    #: to the "_content" section.
    parent_name = None

    def __init__(
        self,
        name: str = None,
        title: str = None,
        subtitle: str = None,
        vargs: dict = None,
    ):
        self.log = alfredlog.QueuedLoggingInterface(base_logger=__name__)

        self._should_be_shown = True

        self._experiment = None
        self._parent_section = None
        self._section = None

        self._title = None
        self._subtitle = None
        self._has_been_shown = False
        self._has_been_hidden = False

        # the following assignments allow for assignment via class variables
        # during subclasses, but override the attributes, if given as
        # init parameters
        self._vargs = _DictObj()
        if vargs is not None:
            self._vargs = _DictObj(vargs)
        elif self.vargs:
            self.vargs = _DictObj(self.vargs)

        if title is not None:
            self.title = title

        if subtitle is not None:
            self.subtitle = subtitle

        self._name_set_via = {}

        if name is not None:
            self._set_name(name, via="argument")
        elif self.name is not None:
            self._set_name(self.name, via="class")

        if self.name is None:
            raise ValueError(f"{type(self).__name__} must be named!")

    @property
    def vargs(self) -> dict:
        """
        A dictionary of additional arguments passed on the class upon
        initialization. Can be defined as a class attribute.

        As a special feature, you can use dot notation to access
        (but not to set) values on this dictionary.

        Examples:

            Example of definig the *vargs* as a class attribute on a page::

                import alfred3 as al

                exp = al.Experiment()


                @exp.member
                class Demo(al.Page):
                    vargs = {"variable_argument": "demo"}

                    def on_exp_access(self):
                        self += al.Text(self.vargs.variable_argument)

            Example of using the *vargs* in page instantiation::

                import alfred3 as al

                exp = al.Experiment()


                class TestPage(al.Page):
                    def on_exp_access(self):
                        self += al.Text(f"{self.vargs.test}")


                @exp.member
                class Test(al.Section):
                    def on_exp_access(self):
                        for i in range(3):
                            self += TestPage(name=f"p{i}", vargs={"test": 123})

        """
        return self._vargs

    def _set_name(self, name: str, via: str):
        """
        Helps organize the different ways a name can be set.

        The ways are:

        1. As a class variable when deriving a page as a new class
        2. As an init argument when instantiating a class

        If a name was set via class variable, the init argument
        can override it.

        """

        check_name(name)
        self._uid = name
        self._tag = name
        self.name = name
        self._name_set_via[via] = self.name

        if len(self._name_set_via) > 1:
            msg = (
                f"Name of {self} was set via multiple methods. Current winner:"
                f" '{self.name}', set via {via}."
            )
            self.log.debug(msg)

    def showif(self) -> bool:
        """
        Hook for controlling whether a page or section should be shown.

        The showif hook is used by overloading. Inside the hook, you
        have access to the current experiment session via ``self.exp``.

        Examples:
            In this examples, the second page is shown only if "yes"
            was entered on the first page::

                import alfred3 as al

                exp = al.Experiment()


                @exp.member
                class Hello(al.Page):
                    def on_exp_access(self):
                        self += al.TextEntry(leftlab="Show second page?", name="el1")


                @exp.member
                class ShowPage(al.Page):
                    title = "Showif Page"

                    def showif(self):
                        return self.exp.values.get("el1") == "yes"

            In this example, the "Main" section is shown only if "yes"
            was entered on the first page::

                import alfred3 as al

                exp = al.Experiment()


                @exp.member
                class Hello(al.Page):
                    def on_exp_access(self):
                        self += al.TextEntry(leftlab="Show second page?", name="el1")


                @exp.member
                class Main(al.Section):
                    def showif(self):
                        return self.exp.values.get("el1") == "yes"

                    def on_exp_access(self):
                        self += al.Page(
                            title="Showif Section Page 1", name="showif_page1"
                        )
                        self += al.Page(
                            title="Showif Section Page 2", name="showif_page2"
                        )

        """
        return True

    @property
    def all_input_elements(self) -> dict:
        """
        Redefined by pages and section.
        """
        return {}

    @property
    def tag(self) -> str:
        """
        Alias for :attr:`.name`, included under this name for backwards
        compatibility. For new code, use *name*.
        """
        return self._tag

    @tag.setter
    def tag(self, tag):
        if not (tag is None or isinstance(tag, str) or isinstance(tag, str)):
            raise TypeError("tag must be an instance of str or unicode")
        if self._tag is not None:
            raise ValueError("you're not allowed to change a tag.")
        self._tag = tag

    @property
    def uid(self) -> str:
        """
        Alias for :attr:`.name`, included under this name for backwards
        compatibility. For new code, use *name*.
        """
        return self._uid

    def visible(self, attr: str) -> dict:
        """
        Returns the subset of members in the given attribute that should
        be shown.

        Args:
            attr (str): Name of an attribute of the class which returns
                a dictionary of members. For instance, sections allow
                'all_members', 'members', 'all_pages', 'pages', and
                pages allow 'elements', 'all_elements', 'input_elements',
                'filled_input_elements'.
        """
        d = getattr(self, attr)
        return {name: value for name, value in d.items() if value.should_be_shown}

    @property
    def should_be_shown(self) -> bool:
        """
        bool: Returns True if should_be_shown is set to True (default) and all
        showif conditions return True.
        """
        cond1 = self._should_be_shown
        cond2 = self.showif()
        return cond1 and cond2

    @should_be_shown.setter
    def should_be_shown(self, b):
        """
        sets should_be_shown to b.

        :type b: bool
        """
        if not isinstance(b, bool):
            raise TypeError("should_be_shown must be an instance of bool")
        self._should_be_shown = b

    @property
    def title(self) -> str:
        """
        Page title (str).
        """
        if self._title is not None:
            return self._title
        else:
            return ""

    @title.setter
    def title(self, title):
        self._title = title

    @property
    def subtitle(self) -> str:
        """
        Page subtitle (str).
        """
        return self._subtitle

    @subtitle.setter
    def subtitle(self, subtitle):
        self._subtitle = subtitle

    def added_to_experiment(self, exp):
        """
        Informs the member that it was added to an experiment session.

        After this method is called, the member has access to the
        experiment session through the attribute :attr:`.exp`
        """
        if not self.name:
            raise AlfredError(f"{type(self).__name__} must have a unique name.")
        self._check_name_uniqueness(exp)
        self._experiment = exp

    def _check_name_uniqueness(self, exp):
        if self.name in exp.root_section.all_updated_members:
            raise AlfredError(
                f"Name '{self.name}' is already present in the experiment."
            )

        # if self.name != list(self._name_set_via.values())[-1]:
        #     raise AlfredError(f"{self}: Name must not be changed after assignment.")

        if self.name in dir(exp):
            raise ValueError(
                "The experiment has an attribute of the same name as"
                f"the page '{self}'. Please choose a different page name."
            )

    @property
    def experiment(self):
        """
        The :class:`.ExperimentSession` to which this member belongs.
        """
        return self._experiment

    @property
    def exp(self):
        """
        The :class:`.ExperimentSession` to which this member belongs.
        """
        return self._experiment

    @exp.setter
    def exp(self, value):
        self._experiment = value

    @experiment.setter
    def experiment(self, value):
        self._experiment = value

    def added_to_section(self, section):
        """
        Informs the member that it was added to a section.
        """
        self._parent_section = section
        self._section = section

    @property
    def section(self):
        """
        The member's parent section.
        """
        return self._section

    @section.setter
    def section(self, section):
        if section is self._section:
            self._section = section
        else:
            raise ValueError("Cannot change a member's section.")

    @property
    def parent(self):
        """
        Alias for :attr:`.section`.
        """
        return self._parent_section

    def uptree(self) -> list:
        """
        List of the parent section and the grandparent sections (recursive).
        """
        out = []
        if self.parent:
            out += [self.parent]
            out += self.parent.uptree()
            return out
        else:
            return out

    @property
    def tree(self) -> str:
        """
        A single string, indicating the member's position in the experiment.
        Consist of the names of the parent section and all grandparent
        sections, separated by dots.
        """
        if not self.parent:
            return self.tag

        if self.parent.tree:
            return self.parent.tree + "." + self.tag
        else:
            return self.tag

    @property
    def short_tree(self) -> str:
        """
        Short version of :attr:`.tree`, without the ``_root._content``
        part that is the same for all members.
        """
        return self.tree.replace("_root._content.", "")

    def position_in_section(self) -> int:
        """
        Returns the position of this page or section inside its parent
        section, starting at 1.

        .. versionadded:: 2.3.0
        """
        members = [m.name for m in self.section.members.values()]
        return members.index(self.name) + 1

    def __str__(self):
        return f"{type(self).__name__}(name='{self.name}')"

    def __repr__(self):
        return self.__str__()
