""" Tree class and functions
"""


from csc148_queue import Queue


class Tree:
    """
    A bare-bones Tree ADT that identifies the root with the entire tree.

    === Attributes ===
    @param object value: value of root node
    @param list[Tree|None] children: child nodes
    """

    def __init__(self, value=None, children=None):
        """
        Create Tree self with content value and 0 or more children

        @param Tree self: this tree
        @param object value: value contained in this tree
        @param list[Tree|None] children: possibly-empty list of children
        @rtype: None
        """
        self._value = value
        # copy children if not None
        # NEVER have a mutable default parameter...
        self._children = children[:] if children is not None else []

    # make self.value and self.children read-only by setting
    # only the get field of their property
    def _get_value(self):
        return self._value
    value = property(_get_value)

    def _get_children(self):
        return self._children
    children = property(_get_children)

    def __repr__(self):
        """
        Return representation of Tree (self) as string that
        can be evaluated into an equivalent Tree.

        @param Tree self: this tree
        @rtype: str

        >>> t1 = Tree(5)
        >>> t1
        Tree(5)
        >>> t2 = Tree(7, [t1])
        >>> t2
        Tree(7, [Tree(5)])
        """
        # Our __repr__ is recursive, because it can also be called
        # via repr...!
        return ('{}({}, {})'.format(self.__class__.__name__, repr(self.value),
                                    repr(self.children))
                if self.children
                else 'Tree({})'.format(repr(self.value)))

    def __eq__(self, other):
        """
        Return whether this Tree is equivalent to other.

        @param Tree self: this tree
        @param object|Tree other: object to compare to self
        @rtype: bool

        >>> t1 = Tree(5)
        >>> t2 = Tree(5, [])
        >>> t1 == t2
        True
        >>> t3 = Tree(5, [t1])
        >>> t2 == t3
        False
        """
        return (type(self) is type(other) and
                self.value == other.value and
                self.children == other.children)

    def __str__(self, indent=0):
        """
        Produce a user-friendly string representation of Tree self,
        indenting each level as a visual clue.

        @param Tree self: this tree
        @param int indent: amount to indent each level of tree
        @rtype: str

        >>> t = Tree(17)
        >>> print(t)
        17
        >>> t1 = Tree(19, [t, Tree(23)])
        >>> print(t1)
           23
        19
           17
        >>> t3 = Tree(29, [Tree(31), t1])
        >>> print(t3)
              23
           19
              17
        29
           31
        """
        root_str = indent * " " + str(self.value)
        mid = len(self.non_none_kids()) // 2
        left_str = [c.__str__(indent + 3)
                    for c in self.non_none_kids()][: mid]
        right_str = [c.__str__(indent + 3)
                     for c in self.non_none_kids()][mid:]
        return '\n'.join(right_str + [root_str] + left_str)

    def non_none_kids(self):
        """ Return a list of Tree self's non-None children.

        @param Tree self:
        @rtype: list[Tree]
        """
        return [c
                for c in self.children
                if c is not None]

    def is_leaf(self):
        """Return whether Tree self is a leaf

        @param Tree self:
        @rtype: bool

        >>> Tree(5).is_leaf()
        True
        >>> Tree(5,[Tree(7)]).is_leaf()
        False
        """
        return len(self.non_none_kids()) == 0

    def __contains__(self, v):
        """
        Return whether Tree self contains v.

        @param Tree self: this tree
        @param object v: value to search this tree for

        >>> t = Tree(17)
        >>> t.__contains__(17)
        True
        >>> t = descendants_from_list(Tree(19), [1, 2, 3, 4, 5, 6, 7], 3)
        >>> t.__contains__(5)
        True
        >>> t.__contains__(18)
        False
        """
        # if len([c
        #         for c in self.children
        #         if c is not None]) == 0:
        #     return self.value == v
        # else:
        #     return (self.value == v
        #             or any([c.__contains__(v)
        #                     for c in self.children
        #                     if c is not None]))
        return (self.value == v
                or any([v in c
                        for c in self.non_none_kids()]))

    def height(self):
        """
        Return length of longest path, + 1, in tree rooted at self.

        @param Tree self:
        @rtype: int

        >>> t = Tree(5)
        >>> t.height()
        1
        >>> t = descendants_from_list(Tree(7), [0, 1, 3, 5, 7, 9, 11, 13], 3)
        >>> t.height()
        3
        """
        if self.is_leaf():
            return 1
        else:
            return 1 + max([c.height()
                            for c in self.non_none_kids()])

    def flatten(self):
        """ Return a list of all values in tree rooted at self.

        @param Tree self:
        @rtype: list

        >>> t = Tree(5)
        >>> t.flatten()
        [5]
        >>> t = descendants_from_list(Tree(7), [0, 1, 3, 5, 7, 9, 11, 13], 3)
        >>> L = t.flatten()
        >>> L.sort()
        >>> L == [0, 1, 3, 5, 7, 7, 9, 11, 13]
        True
        """
        if self.is_leaf():
            return [self.value]
        else:
            return ([self.value]
                    + sum([c.flatten()
                           for c in self.non_none_kids()], []))


class BinaryTree(Tree):
    """ Trees with branching factor of 2.

    === Attributes ===
    @param BinaryTree|None left: left child, aliases children[0]
    @param BinaryTree|None right: right child, aliases children[1]
    """

    def __init__(self, value=None, left=None, right=None):
        """ Create BinaryTree self with value, left and right children.

        Extends Tree

        @param BinaryTree self: this binary tree
        @param object value:
        @param BinaryTree|None left:
        @param BinaryTree|None right:
        @rtype: None
        """
        Tree.__init__(self, value, [left, right])

    # create properties left and right as aliases for
    # children[0] and children[1]
    def _set_left(self, left):
        self.children[0] = left

    def _get_left(self):
        return self.children[0]

    left = property(_get_left, _set_left)

    def _set_right(self, right):
        self.children[1] = right

    def _get_right(self):
        return self.children[1]

    right = property(_get_right, _set_right)


def leaf_count(t):
    """
    Return the number of leaves in Tree t.

    @param Tree t: tree to count the leaves of
    @rtype: int

    >>> t = Tree(7)
    >>> leaf_count(t)
    1
    >>> t = descendants_from_list(Tree(7), [0, 1, 3, 5, 7, 9, 11, 13], 3)
    >>> leaf_count(t)
    6
    """
    if t.is_leaf():
        return 1
    else:
        return sum([leaf_count(c)
                    for c in t.non_none_kids()])


def count_if(t, p):
    """ Return number of values that satisfy p in t.

    Assume that all values in t are valid input for p.

    >>> t = Tree(7)
    >>> def p(v): return v > 6
    >>> count_if(t, p)
    1
    >>> t = descendants_from_list(Tree(9), [3, 5, 7, 11], 2)
    >>> count_if(t, p)
    3
    """
    # if len([c
    #         for c in t.children
    #         if c is not None]) == 0:
    #     return 1 if p(t.value) else 0
    # else:
    #     return  (1 if p(t.value) else 0) + sum([count_if(c, p)
    #                     for c in t.children
    #                     if c is not None])
    return (1 if p(t.value) else 0) + sum([count_if(c, p)
                                           for c in t.non_none_kids()])


def preorder_visit(t, act):
    """
    Visit each node of Tree t in preorder, and act on the nodes
    as they are visited.

    @param Tree t: tree to visit in preorder
    @param (Tree)->Any act: function to carry out on visited Tree node
    @rtype: None

    >>> t = descendants_from_list(Tree(0), [1, 2, 3, 4, 5, 6, 7], 3)
    >>> def act(node): print(node.value)
    >>> preorder_visit(t, act)
    0
    1
    4
    5
    6
    2
    7
    3
    """
    # if len([c
    #         for c in t.children
    #         if c is not None]) == 0:
    #     act(t)
    # else:
    #     act(t)
    #     for c in t.children:
    #         if c is not None:
    #             preorder_visit(c, act)
    act(t)
    for c in t.children:
        if c is not None:
            preorder_visit(c, act)


def postorder_visit(t, act):
    """
    Visit each node of t in postorder, and act on it when it is visited.

    @param Tree t: tree to be visited in postorder
    @param (Tree)->Any act: function to do to each node
    @rtype: None

    >>> t = descendants_from_list(Tree(0), [1, 2, 3, 4, 5, 6, 7], 3)
    >>> def act(node): print(node.value)
    >>> postorder_visit(t, act)
    4
    5
    6
    1
    7
    2
    3
    0
    """
    for c in t.children:
        if c is not None:
            postorder_visit(c, act)
    act(t)


def levelorder_visit(t, act):
    """
    Visit every node in Tree t in level order and act on the node
    as you visit it.

    @param Tree t: tree to visit in level order
    @param (Tree)->Any act: function to execute during visit

    >>> t = descendants_from_list(Tree(0), [1, 2, 3, 4, 5, 6, 7], 3)
    >>> def act(node): print(node.value)
    >>> levelorder_visit(t, act)
    0
    1
    2
    3
    4
    5
    6
    7
    """
    visit_queue = Queue()
    visit_queue.add(t)
    while not visit_queue.is_empty():
        next_tree = visit_queue.remove()
        for c in next_tree.children:
            if c is not None:
                visit_queue.add(c)
        act(next_tree)


# helpful helper function
def descendants_from_list(t, list_, arity):
    """
    Populate Tree t's descendants from list_, filling them
    in in level order, with up to arity children per node.
    Then return t.

    @param Tree t: tree to populate from list_
    @param list list_: list of values to populate from
    @param int arity: maximum branching factor
    @rtype: Tree

    >>> descendants_from_list(Tree(0), [1, 2, 3, 4], 2)
    Tree(0, [Tree(1, [Tree(3), Tree(4)]), Tree(2)])
    """
    q = Queue()
    q.add(t)
    list_ = list_.copy()
    while not q.is_empty():  # unlikely to happen
        new_t = q.remove()
        for i in range(0, arity):
            if len(list_) == 0:
                return t  # our work here is done
            else:
                new_t_child = Tree(list_.pop(0))
                new_t.children.append(new_t_child)
                q.add(new_t_child)
    return t


if __name__ == '__main__':
    import doctest
    doctest.testmod()
