from prettytoml import tokens
from prettytoml.elements import common
from prettytoml.elements.metadata import PunctuationElement, NewlineElement
from prettytoml.elements.traversal import predicates


class TraversalMixin:
    """
    A mix-in that provides convenient sub-element traversal to any class with
    an `elements` member that is a sequence of Element instances
    """

    def __find_following_element(self, index, predicate):
        """
        Finds and returns the index of element in self.elements that evaluates the given predicate to True
        and whose index is higher than the given index, or returns -Infinity on failure.
        """
        return find_following(self.elements, predicate, index)

    def __find_preceding_element(self, index, predicate):
        """
        Finds and returns the index of the element in self.elements that evaluates the given predicate to True
        and whose index is lower than the given index.
        """
        i = find_previous(self.elements, predicate, index)
        if i == float('inf'):
            return float('-inf')
        return i

    def __must_find_following_element(self, predicate):
        """
        Finds and returns the index to the element in self.elements that evaluatest the predicate to True, or raises
        an error.
        """
        i = self.__find_following_element(-1, predicate)
        if i < 0:
            raise RuntimeError('Could not find non-optional element')
        return i

    def _enumerate_non_metadata_sub_elements(self):
        """
        Returns a sequence of of (index, sub_element) of the non-metadata sub-elements.
        """
        return ((i, element) for i, element in enumerate(self.elements) if element.type != common.TYPE_METADATA)

    def _find_preceding_comma(self, index):
        """
        Returns the index of the preceding comma element to the given index, or -Infinity.
        """
        return self.__find_preceding_element(index, predicates.op_comma)

    def _find_following_comma(self, index):
        """
        Returns the index of the following comma element after the given index, or -Infinity.
        """
        def predicate(element):
            return isinstance(element, PunctuationElement) and element.token.type == tokens.TYPE_OP_COMMA
        return self.__find_following_element(index, predicate)

    def _find_following_newline(self, index):
        """
        Returns the index of the following newline element after the given index, or -Infinity.
        """
        return self.__find_following_element(index, lambda e: isinstance(e, NewlineElement))

    def _find_following_comment(self, index):
        """
        Returns the index of the following comment element after the given index, or -Infinity.
        """
        return self.__find_following_element(index, predicates.comment)

    def _find_following_line_terminator(self, index):
        """
        Returns the index of the following comment or newline element after the given index, or -Infinity.
        """
        following_comment = self._find_following_comment(index)
        following_newline = self._find_following_newline(index)

        if following_comment == float('-inf'):
            return following_newline
        if following_newline == float('inf'):
            return following_comment

        if following_newline < following_comment:
            return following_newline
        else:
            return following_comment

    def _find_preceding_newline(self, index):
        """
        Returns the index of the preceding newline element to the given index, or -Infinity.
        """
        return self.__find_preceding_element(index, predicates.newline)

    def _find_following_non_metadata(self, index):
        """
        Returns the index to the following non-metadata element after the given index, or -Infinity.
        """
        return self.__find_following_element(index, predicates.non_metadata)

    def _find_closing_square_bracket(self):
        """
        Returns the index to the closing square bracket, or raises an Error.
        """

        return self.__must_find_following_element(predicates.closing_square_bracket)

    def _find_following_opening_square_bracket(self, index):
        """
        Returns the index to the opening square bracket, or -Infinity.
        """
        return self.__find_following_element(index, predicates.opening_square_bracket)

    def _find_following_closing_square_bracket(self, index):
        """
        Returns the index to the closing square bracket, or -Infinity.
        """
        return self.__find_following_element(index, predicates.closing_square_bracket)

    def _find_following_table(self, index):
        """
        Returns the index to the next TableElement after the specified index, or -Infinity.
        """
        return self.__find_following_element(index, predicates.table)

    def _find_preceding_table(self, index):
        """
        Returns the index to the preceding TableElement to the specified index, or -Infinity.
        """
        return self.__find_preceding_element(index,predicates.table)

    def _find_closing_curly_bracket(self):
        """
        Returns the index to the closing curly bracket, or raises an Error.
        """
        def predicate(element):
            return isinstance(element, PunctuationElement) and element.token.type == tokens.TYPE_OP_CURLY_RIGHT_BRACKET
        return self.__must_find_following_element(predicate)

    def _find_following_table_header(self, index):
        """
        Returns the index to the table header after the given element index, or -Infinity.
        """
        return self.__find_following_element(index, predicates.table_header)


def find_following(element_seq, predicate, index=None):
    """
    Finds and returns the index of the next element fulfilling the specified predicate after the specified
    index, or -Infinity.

    Starts searching linearly from the start_from index.
    """

    if isinstance(index, (int, float)) and index < 0:
        index = None

    for i, element in tuple(enumerate(element_seq))[index+1 if index is not None else index:]:
        if predicate(element):
            return i
    return float('-inf')


def find_previous(element_seq, predicate, index=None):
    """
    Finds and returns the index of the previous element fulfilling the specified predicate preceding to the specified
    index, or Infinity.
    """
    if isinstance(index, (int, float)) and index >= len(element_seq):
        index = None

    for i, element in reversed(tuple(enumerate(element_seq))[:index]):
        if predicate(element):
            return i
    return float('inf')
