"""
BinaryTree class and associated functions.
"""
from typing import Union
import sys

class BinaryTree:
    """
    A Binary Tree, i.e. arity 2.
    """
    def __init__(self, value, left=None, right=None):
        """
        Create BinaryTree self with value and children left and right.
        @param BinaryTree self: this binary tree
        @param object value: value of this node
        @param BinaryTree|None left: left child
        @param BinaryTree|None right: right child
        @rtype: None
        """
        self.value, self.left, self.right = value, left, 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.value == other.value 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.value),
                                               repr(self.left),
                                               repr(self.right))

    def __str__(self, indent=""):
        """
        Return a user-friendly string representing BinaryTree (self)
        inorder.  Indent by indent.
        >>> childTree1 = BinaryTree(2,BinaryTree(3))
        >>> childTree2 = BinaryTree(4)
        >>> b = BinaryTree(1,childTree1, childTree2)

        >>> 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, val:object)->bool:
        """
        Return whether tree rooted at self contains value.
        @param BinaryTree self: binary tree to search for value
        @param object value: value to search for
        @rtype: bool
        >>> t = BinaryTree(5, BinaryTree(7), BinaryTree(9))
        >>> t.__contains__(7)
        True
        """
        if self.right is None and self.left is None:
            return self.value == val
        elif self.right is None:
            return self.value == val or self.left.__contains__(val)
        elif self.left is None:
            return self.value == val or self.right.__contains__(val)
        else:
            return self.value == val or any([self.right.__contains__(val),
                                             self.left.__contains__(val)])
        #one liner
        #return (self.value == val
        #        or (self.left is not None and val in self.left)
        #        or (self.right is not None and val in self.right))


def contains(node:BinaryTree, val:object) -> bool:
    """
    Return whether tree rooted at node contains value.
    @param BinaryTree|None node: binary tree to search for value
    @param object val: value to search for
    @rtype: bool
    >>> contains(None, 5)
    False
    >>> contains(BinaryTree(5, BinaryTree(7), BinaryTree(9)), 7)
    True
    """
    if node is None:
        return False
    else:
        return node.value == val or any([contains(node.right, val),
                                         contains(node.left, val)])


def height(node:BinaryTree) -> int:
    """
    Return the height of the binary tree rooted at node
    @param BinaryTree node: A binary tree node
    @return: int

    >>> height(None)
    0
    >>> height(BinaryTree(5, BinaryTree(7, BinaryTree(8)), BinaryTree(9)))
    3
    """
    if node is None:
        return 0
    else:
        return 1+ max(height(node.left), height(node.right))


def find(node:BinaryTree, val:object) -> BinaryTree:
    """
    Return a BinaryTree node that contains the given value
    @param BinaryTree node: A binary tree node
    @param object val: value to search
    @return: BinaryTree

    >>> find(None, 5) is None
    True
    >>> find(BinaryTree(5, BinaryTree(7), BinaryTree(9)), 7)
    BinaryTree(7, None, None)
    """
    if node is None:
        return None
    else:
        if node.value == val:
            return node
        elif find(node.left, val) is not None:
            return find(node.left, val)
        elif find(node.right, val) is not None:
            return find(node.right, val)
        else:
            return None


def evaluate(b:BinaryTree) -> float:
    """
    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

     @param BinaryTree b: binary tree representing arithmetic expression
     @rtype: float

    >>> 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(str(evaluate(b.left))
                    +b.value
                    +str(evaluate(b.right)))


def parenthesize(b:BinaryTree) -> str:
    """
    Parenthesize the expression rooted at b.  If b is a leaf,
    return its float value.  Otherwise, parenthesize 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

     @param BinaryTree b: binary tree representing arithmetic expression
     @rtype: str

    >>> b = BinaryTree(3.0)
    >>> print(parenthesize(b))
    3.0
    >>> b = BinaryTree("+", BinaryTree("*", BinaryTree(3.0), BinaryTree(4.0)), BinaryTree(7.0))
    >>> print(parenthesize(b))
    ((3.0 * 4.0) + 7.0)
    """
    if b.left is None and b.right is None:
        return str(b.value)
    else:
        return "({} {} {})".format(parenthesize(b.left),
                                   b.value,
                                   parenthesize(b.right))


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

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

    >>> def f(node): print(node.value)
    >>> b = None
    >>> inorder_visit(b, f) is None
    True
    >>> b = BinaryTree("+", BinaryTree("*", BinaryTree(3.0), BinaryTree(4.0)), BinaryTree(7.0))
    >>> inorder_visit(b, f)
    3.0
    *
    4.0
    +
    7.0
    """
    if b is None:
        return None
    else:
        inorder_visit(b.left, act)
        act(b)
        inorder_visit(b.right, act)


def preorder_visit(b, act):
    """
    Visit BinaryTree t in preorder 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

    >>> def f(node): print(node.value)
    >>> b = None
    >>> preorder_visit(b, f) is None
    True
    >>> b = BinaryTree("+", BinaryTree("*", BinaryTree(3.0), BinaryTree(4.0)), BinaryTree(7.0))
    >>> preorder_visit(b, f)
    +
    *
    3.0
    4.0
    7.0
    """
    if b is None:
        return None
    else:
        act(b)
        preorder_visit(b.left, act)
        preorder_visit(b.right, act)


def postorder_visit(b, 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

    >>> def f(node): print(node.value)
    >>> b = None
    >>> postorder_visit(b, f) is None
    True
    >>> b = BinaryTree("+", BinaryTree("*", BinaryTree(3.0), BinaryTree(4.0)), BinaryTree(7.0))
    >>> postorder_visit(b, f)
    3.0
    4.0
    *
    7.0
    +
    """
    if b is None:
        return None
    else:
        postorder_visit(b.left, act)
        postorder_visit(b.right, act)
        act(b)


# assume binary search tree order property
def bst_contains(node, value):
    """
    Return whether tree rooted at node contains value.

    Assume node is the root of a Binary Search Tree

    @param BinaryTree|None node: node of a Binary Search Tree
    @param object value: value to search for
    @rtype: bool

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


def bst_distance(node:BinaryTree, val:object) -> int:
    """
    Find distance of a node with the value from the root

    @param BinaryTree node: The binary tree
    @param object val: Value to find in the node
    @rtype: int

    >>> tree = BinaryTree(4)
    >>> bst_distance(tree, 4)
    0
    >>> tree = BinaryTree(4, BinaryTree(3, BinaryTree(1)), BinaryTree(5))
    >>> bst_distance(tree, 1)
    2
    """
    pass


def is_bst(node:Union[BinaryTree, None]) -> bool:
    """
    Checks whether the Binary Tree rooted at node is a BST
    @param BinaryTree|None node: The binary tree
    @return: bool

    >>> is_bst(None)
    True
    >>> tree = BinaryTree(4, BinaryTree(3, BinaryTree(1)), BinaryTree(5))
    >>> is_bst(tree)
    True
    >>> tree = BinaryTree(3, BinaryTree(2, BinaryTree(1), BinaryTree(4)), BinaryTree(5))
    >>> is_bst(tree)
    False
    """

    if node is None:
        return True
    else:
        left_result = True
        right_result = True
        if node.left is not None:
            left_result =  node.left.value < node.value
        if node.right is not None:
            right_result = node.right.value > node.value
        return all([left_result, right_result, is_bst(node.left), is_bst(node.right)])


def tree_add(node: Union[BinaryTree, None], num: float) -> BinaryTree:
    """
    Adds num to each node of the Binary Tree and return a modified Tree
    Assumes the

    @param BinaryTree|None node: The binary tree
    @param float num: number to add
    @rtype: BinaryTree

    >>> tree_add(None, 5) is None
    True
    >>> tree_add(BinaryTree(2, BinaryTree(1), BinaryTree(3)), 2)
    BinaryTree(4, BinaryTree(3, None, None), BinaryTree(5, None, None))
    """
    pass


def insert(node: Union[BinaryTree, None], value: object) -> BinaryTree:
    """
    Insert value in BST rooted at node if necessary, and return new root.

    Assume node is the root of a Binary Search Tree.

    @param BinaryTree|None node: root of a binary search tree.
    @param object value: value to insert into BST, if necessary.
    @rtype: BinaryTree

    >>> insert(None, 5)
    BinaryTree(5, None, None)
    >>> b = BinaryTree(5)
    >>> b1 = insert(b, 3)
    >>> print(b1)
    5
        3
    <BLANKLINE>
    """
    pass


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