"""binary tree code and examples
"""

from csc148_queue import Queue
from typing import Callable, Union, Any


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

    def __init__(self, value: object, left: Union['BinaryTree', None]=None,
                 right: Union['BinaryTree', None]=None) -> None:
        """
        Create BinaryTree self with value and children left and right.

        """
        self.value, self.left, self.right = value, left, right

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

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

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

        >>> BinaryTree(1, BinaryTree(2), BinaryTree(3))
        BinaryTree(1, BinaryTree(2, None, None), BinaryTree(3, None, None))
        """
        return "BinaryTree({}, {}, {})".format(repr(self.value),
                                               repr(self.left),
                                               repr(self.right))
    def __str__(self, indent="") -> str:
        """
        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 else "")
        left_tree = self.left.__str__(indent + "    ") if self.left else ""
        return (right_tree + "{}{}\n".format(indent, str(self.value)) +
                left_tree)

    def __contains__(self, value: object) -> bool:
        """
        Return whether tree rooted at self contains value.

        >>> t = BinaryTree(5, BinaryTree(7), BinaryTree(9))
        >>> 7 in t
        True
        >>> t = BinaryTree(5, BinaryTree(7), None)
        >>> 3 in t
        False
        """
        if self.left is None and self.right is None:
            return self.value == value
        elif self.left is None:
            return self.value == value or value in self.right
        elif self.right is None:
            return self.value == value or value in self.left
        else:
            return (self.value == value
                    or value in self.left
                    or value in self.right)

        # another solution
        # return (self.value == value
        #         or (self.left is not None and value in self.left)
        #         or (self.right is not None and value in self.right))


def contains(node: Union[BinaryTree, None], value: object) -> bool:
    """
    Return whether tree rooted at self contains value.

    >>> t = BinaryTree(5, BinaryTree(7), BinaryTree(9))
    >>> contains(t,5)
    True
    >>> contains(t,2)
    False
    >>> t1 = BinaryTree(5, BinaryTree(7,BinaryTree(3)), None)
    >>> contains(t1,1)
    False
    """
    if node is None:
        return False
    elif node.left is None and node.right is None:
        return node.value == value
    else:
        return (node.value == value
                or contains(node.left, value)
                or contains(node.right, value))
    # another solution
    # if node.left is None and node.right is None:
    #     return node.value == value
    # elif node.left is None:
    #     return node.value == value or contains(node.right, value)
    # elif node.right is None:
    #     return node.value == value or contains(node.left, value)
    # else:
    #     return (node.value == value
    #             or contains(node.left, value)
    #             or contains(node.right, value))

    # another solution
    # if node.left is None and node.right is None:
    #     return node.value == value
    # else:
    #     return (node.value == value
    #             or (node.left is not None and contains(node.left,value))
    #             or (node.right is not None and contains(node.right,value)))

def evaluate(b: BinaryTree) -> Union[float, object]:
    """
    Evaluate the expression rooted at b.  If b is a leaf,
    return its float value.  Otherwise, evaluate b.left and
    b.right and combine them with b.value.

    Assume:  -- b is a non-empty binary tree
             -- interior nodes contain value in {"+", "-", "*", "/"}
             -- interior nodes always have two children
             -- leaves contain float value

    >>> b = BinaryTree(3.0)
    >>> evaluate(b)
    3.0
    >>> b = BinaryTree("*", BinaryTree(3.0), BinaryTree(4.0))
    >>> evaluate(b)
    12.0
    """
    if b.left is None and b.right is None:
        return b.value
    else:
        return eval("{} {} {}".format(evaluate(b.left),
                                      b.value,
                                      evaluate(b.right)))

def height(node: Union[BinaryTree, None]) -> int:
    """
    Return heigh of Binary tree.

    >>> t = BinaryTree(5, BinaryTree(7), None)
    >>> height(t)
    2
    """
    if node is None:
        return 0
    else:
        return 1 + max(height(node.left),height(node.right))

def find(node: Union[BinaryTree, None], data: object)-> Union[BinaryTree, None]:
    """
    Return BinaryTree containing data or else None.

    >>> find(None,15) is None
    True
    >>> bt = BinaryTree(5, BinaryTree(4),BinaryTree(3))
    >>> find(bt,7) is None
    True
    >>> find(bt,4)
    BinaryTree(4, None, None)
    >>> find(bt,3)
    BinaryTree(3, None, None)
    """
    # print('node:', node.value if node is not None else "None")
    if node is None:
        return None
    else:
        if node.value == data:
            return node
        elif find(node.left,data) is not None:
            return find(node.left,data)
        elif find(node.right,data) is not None:
            return find(node.right,data)
        else:
            return None
    # Another solution
    # if node is None:
    #     return None
    # else:
    #     if node.value == data:
    #         return node
    #     elif find(node.left,data) is not None:
    #         return find(node.left,data)
    #     else:
    #         return find(node.right, data)
def postorder_visit(t: BinaryTree, act: Callable[[BinaryTree], Any]) -> None:
    """
    Visit BinaryTree t in postorder and act on nodes as you visit.

    >>> bt = BinaryTree(5, BinaryTree(7), BinaryTree(9))
    >>> def f(node): print(node.value)
    >>> postorder_visit(bt, f)
    7
    9
    5
    """
    if t is None:
        pass
    else:
        postorder_visit(t.left, act)
        postorder_visit(t.right, act)
        act(t)

def preorder_visit(t: BinaryTree, act: Callable[[BinaryTree], Any]) -> None:
    """
    Visit BinaryTree t in preorder and act on nodes as you visit.

    >>> bt = BinaryTree(5, BinaryTree(7), BinaryTree(9))
    >>> def f(node): print(node.value)
    >>> preorder_visit(bt, f)
    5
    7
    9
    """
    if t is None:
        pass
    else:
        act(t)
        preorder_visit(t.left, act)
        preorder_visit(t.right, act)

def inorder_visit(t: BinaryTree, act: Callable[[BinaryTree], Any]) -> None:
    """
    Visit each node of binary tree rooted at root in order and act.

    >>> bt = BinaryTree(5, BinaryTree(7), BinaryTree(9))
    >>> def f(node): print(node.value)
    >>> inorder_visit(bt, f)
    7
    5
    9
    """
    if t is None:
        pass
    else:
        inorder_visit(t.left, act)
        act(t)
        inorder_visit(t.right, act)
    # exercise: do this as a method!

def bst_contains(node: BinaryTree, value: object) ->bool:
    """
    Return whether tree rooted at node contains value.

    Assume node is the root of a Binary Search Tree

    >>> bst_contains(None, 5)
    False
    >>> bst_contains(BinaryTree(7, BinaryTree(5), BinaryTree(9)), 5)
    True
    """
    if node is None:
        return False
    elif node.value > value:
        return bst_contains(node.left, value)
    elif node.value < value:
        return bst_contains(node.right, value)
    else:
        return True

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

bt3 = BinaryTree(3, None,BinaryTree(6))
bt2 = BinaryTree(4, BinaryTree(0),BinaryTree(8))
bt1 = BinaryTree(5, bt2,bt3)
# to trace find uncoment the print in find
print (find(bt1,7))

