"""
BinaryTree class and associated functions.
"""


class BinaryTree:
    """
    A Binary Tree, i.e. arity 2.
    """

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

        @param BinaryTree self: this binary tree
        @param object data: data of this node
        @param BinaryTree|None left: left child
        @param BinaryTree|None right: right child
        @rtype: None
        """
        self.data = data
        self.left = left
        self.right = right

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

        @param BinaryTree self: this binary tree
        @param Any other: object to check equivalence to self
        @rtype: bool

        >>> BinaryTree(7).__eq__("seven")
        False
        >>> b1 = BinaryTree(7, BinaryTree(5))
        >>> b1.__eq__(BinaryTree(7, BinaryTree(5), None))
        True
        """
        return (type(self) == type(other) and
                self.data == other.data and
                (self.left, self.right) == (other.left, other.right))

    def __repr__(self):
        """
        Represent BinaryTree (self) as a string that can be evaluated to
        produce an equivalent BinaryTree.

        @param BinaryTree self: this binary tree
        @rtype: str

        >>> BinaryTree(1, BinaryTree(2), BinaryTree(3))
        BinaryTree(1, BinaryTree(2, None, None), BinaryTree(3, None, None))
        """
        return "BinaryTree({}, {}, {})".format(repr(self.data),
                                               repr(self.left),
                                               repr(self.right))

    def __str__(self, indent=""):
        """
        Return a user-friendly string representing BinaryTree (self)
        inorder.  Indent by indent.

        >>> b = BinaryTree(1, BinaryTree(2, BinaryTree(3)), BinaryTree(4))
        >>> print(b)
            4
        1
            2
                3
        <BLANKLINE>
        """
        right_tree = (self.right.__str__(
            indent + "    ") if self.right is not None else "")
        left_tree = self.left.__str__(indent + "    ") if self.left else ""
        return (right_tree + "{}{}\n".format(indent, str(self.data)) +
                left_tree)

    def contains(self, value):
        """
        Return whether tree rooted at node contains value.

        @param BinaryTree|None self: binary tree to search for value
        @param object value: value to search for
        @rtype: bool

        >>> b = BinaryTree(5, BinaryTree(7), BinaryTree(9))
        >>> b.contains(7)
        True
        >>> b.contains(8)
        False
        """
        # handling the None case will be trickier for a method
        contains_left = False
        if self.left is not None:
            contains_left = self.left.contains(value)
        contains_right = (self.right is not None and
                          self.right.contains(value))
        return (self.data == value or
                contains_left or contains_right)


def postorder_visit(t, act):
    """
    Visit BinaryTree t in postorder and act on nodes as you visit.

    @param BinaryTree|None t: binary tree to visit
    @param (BinaryTree)->Any act: function to use on nodes
    @rtype: None

    >>> b1 = BinaryTree(4, BinaryTree(2), BinaryTree(6))
    >>> b2 = BinaryTree(12, BinaryTree(10), BinaryTree(14))
    >>> b = BinaryTree(8, b1, b2)
    >>> def f(node): print(node.data)
    >>> postorder_visit(b, f)
    2
    6
    4
    10
    14
    12
    8
    """
    # left, right, root
    # visit left child, if not None
    if t is not None:
        postorder_visit(t.left, act)
        postorder_visit(t.right, act)
        act(t)


def inorder_visit(root, act):
    """
    Visit each node of binary tree rooted at root in order and act.

    @param BinaryTree root: binary tree to visit
    @param (BinaryTree)->object act: function to execute on visit
    @rtype: None

    >>> b1 = BinaryTree(4, BinaryTree(2), BinaryTree(6))
    >>> b2 = BinaryTree(12, BinaryTree(10), BinaryTree(14))
    >>> b = BinaryTree(8, b1, b2)
    >>> def f(node): print(node.data)
    >>> inorder_visit(b, f)
    2
    4
    6
    8
    10
    12
    14
    """
    if root is not None:
        inorder_visit(root.left, act)
        act(root)  # BinaryTree.act(root)
        inorder_visit(root.right, act)


def visit_level(t, n, act):
    """
    Visit each node of BinaryTree t at level n and act on it.  Return
    the number of nodes visited visited.

    @param BinaryTree|None t: binary tree to visit
    @param int n: level to visit
    @param (BinaryTree)->Any act: function to execute on nodes at level n
    @rtype: int

    >>> b1 = BinaryTree(4, BinaryTree(2), BinaryTree(6))
    >>> b2 = BinaryTree(12, BinaryTree(10), BinaryTree(14))
    >>> b = BinaryTree(8, b1, b2)
    >>> def f(node): print(node.data)
    >>> visit_level(b, 2, f)
    2
    6
    10
    14
    4
    """
    if t is None:
        return 0
    elif n == 0:
        act(t)
        return 1
    elif n > 0:
        return (visit_level(t.left, n-1, act) +
                visit_level(t.right, n-1, act))
    else:
        return 0


def levelorder_visit(t, act):
    """
    Visit BinaryTree t in level order and act on each node.

    @param BinaryTree|None t: binary tree to visit
    @param (BinaryTree)->Any act: function to use during visit
    @rtype: None

    >>> b1 = BinaryTree(4, BinaryTree(2), BinaryTree(6))
    >>> b2 = BinaryTree(12, BinaryTree(10), BinaryTree(14))
    >>> b = BinaryTree(8, b1, b2)
    >>> def f(node): print(node.data)
    >>> levelorder_visit(b, f)
    8
    4
    12
    2
    6
    10
    14
    """
    # this approach uses iterative deepening
    visited, n = visit_level(t, 0, act), 0
    while visited > 0:
        n += 1
        visited = visit_level(t, n, act)


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