# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX
# All rights reserved.
#
# This software is provided without warranty under the terms of the BSD
# license included in LICENSE.txt and may be redistributed only under
# the conditions described in the aforementioned license. The license
# is also available online at http://www.enthought.com/licenses/BSD.txt
#
# Thanks for using Enthought open source!

""" Define the ViewElements class, which is used to define a (typically
    class-based) hierarchical name space of related ViewElement objects.

    Normally there is a ViewElements object associated with each Traits-based
    class, which contains all of the ViewElement objects associated with the
    class. The ViewElements object is also linked to the ViewElements objects
    of its associated class's parent classes.
"""

from traits.api import HasStrictTraits, List, Dict, Str, Int, Any, TraitError

from .view_element import ViewElement

# -------------------------------------------------------------------------
#  Trait definitions:
# -------------------------------------------------------------------------

# Trait for contents of a ViewElements object
content_trait = Dict(str, ViewElement)


class ViewElements(HasStrictTraits):
    """Defines a hierarchical name space of related ViewElement objects."""

    # -------------------------------------------------------------------------
    #  Trait definitions:
    # -------------------------------------------------------------------------

    #: Dictionary containing the named ViewElement items
    content = content_trait

    def find(self, name, stack=None):
        """Finds a specified ViewElement within the specified (optional) search
        context.
        """
        # Assume search starts from the beginning the of the search order:
        i = 0

        # If a stack was specified, see if there is a matching entry in the
        # stack already:
        if stack is not None:
            for ssi in stack:
                if name == ssi.id:
                    # Match found, resume search at next ViewElements object
                    # in the search order:
                    i = ssi.context + 1
                    break

        # Search for a matching name starting at the specified ViewElements
        # object in the search order:
        for j, ves in enumerate(self._get_search_order()[i:]):
            result = ves.content.get(name)
            if result is not None:
                # Match found. If there is a stack, push matching name and
                # ViewElements context onto it:
                if stack is not None:
                    stack[0:0] = [SearchStackItem(id=name, context=i + j)]

                # Return the ViewElement object that matched the name:
                return result

        # Indicate no match was found:
        return None

    def filter_by(self, klass=None):
        """Returns a sorted list of all names accessible from the ViewElements
        object that are of a specified (ViewElement) type.
        """
        if klass is None:
            from . import view

            klass = view.View
        result = []

        # Add each item in the search order which is of the right class and
        # which is not already in the result list:
        for ves in self._get_search_order():
            for name, ve in ves.content.items():
                if isinstance(ve, klass) and (name not in result):
                    result.append(name)

        # Sort the resulting list of names:
        result.sort()

        # Return the result:
        return result

    def _parents__changed(self):
        self._search_order = None

    def _parents_items_changed(self):
        self._search_order = None

    def _get_search_order(self):
        if self._search_order is None:
            self._search_order = self._mro()
        return self._search_order

    # -------------------------------------------------------------------------
    #  Compute the Python 'C3' algorithm used to determine a class's 'mro'
    #  and apply it to the 'parents' of the ViewElements to determine the
    #  correct search order:
    # -------------------------------------------------------------------------

    def _mro(self):
        return self._merge(
            [[self]]
            + [parent._get_search_order()[:] for parent in self.parents]
            + [self.parents[:]]
        )

    def _merge(self, seqs):
        result = []
        while True:
            # Remove any empty sequences from the list:
            seqs = [seq for seq in seqs if len(seq) > 0]
            if len(seqs) == 0:
                return result

            # Find merge candidates among the sequence heads:
            for seq in seqs:
                candidate = seq[0]
                if len([s for s in seqs if candidate in s[1:]]) == 0:
                    break
            else:
                raise TraitError("Inconsistent ViewElements hierarchy")

            # Add the candidate to the result:
            result.append(candidate)

            # Then remove the candidate:
            for seq in seqs:
                if seq[0] == candidate:
                    del seq[0]

    def __repr__(self):
        """Returns a "pretty print" version of the ViewElements object."""
        return self.content.__repr__()


# -------------------------------------------------------------------------
#  Define forward reference traits:
# -------------------------------------------------------------------------

ViewElements.add_class_trait("parents", List(ViewElements))
ViewElements.add_class_trait("_search_order", Any)


class SearchStackItem(HasStrictTraits):

    # -------------------------------------------------------------------------
    #  Trait definitions:
    # -------------------------------------------------------------------------

    #: Name that was looked up
    id = Str()

    #: Index into the 'mro' list of ViewElements that the ID was found in
    context = Int()
