import random
# ============================================================================
#                         N O D E
# ============================================================================

class Node:
    def __init__(self, key):

        self.left = None     # mandatory
        self.right = None    # mandatory
        self.key = key       # mandatory

        # optional
        self.tag = ' ' # one character for demonstation purposes here

# ============================================================================
#                         B I N A R Y   T R E E
# ============================================================================

class BinaryTree:

    # .......Constructor ...........................................
    # Create initial binary tree with one node -- the root
    def __init__( self, key = 0 ):
        self.root = Node( key )

    # ........................................................................
    #       S E R V I C E     F U N C T O N S
    # ......IMPORTED..........................................................
    # The following BinaryTree methods are part of the present class BinaryTree,
    # they are stored separately in imported files, to improve readability of this file,
    from _treebuild_   import randomTree, addNode, addNodes
    from _treedisplay_ import display
    # def randomTree( self, node, depth ):
    # def addnode( self, parentKey, nodeKey ):
    # def addNodes( self, nodePairs ):          # pair == (parentKey, nodeKey)
    # def display( self ):
    # For purely technical reasons, other private functions are additionally imported
    from _treebuild_   import _addnoder
    from _treedisplay_ import _setXcoord, _countNodes
    # .......................................................................


    # ........................................................................
    #   C U S T O M   F U N C T I O N (S),   T E S T E D  I N   M A I N (below)
    # .............................................................. .........

    def keysToDepths(self, node, depth):
        if node == None: return
        node.key = depth
        self.keysToDepths( node.left, depth + 1 )
        self.keysToDepths( node.right, depth + 1 )

    # node height == distance form node to its deepest descendant
    # leaf height = 0
    def keysToHeights(self, node ):
        if node == None: return -1
        height = 1 + max( self.keysToHeights(node.left), \
                          self.keysToHeights(node.right) )
        node.key = height
        return height

    def insertLeftmost(self, node, key):
        if node.left == None:
            node.left = Node(key)
            return
        self.insertLeftmost( node.left, key )

    def deleteAllLeaves(self, node):
        childL = node.left
        childR = node.right

        if childL != None:
            if childL.left == childL.right == None:  # childL is a leaf
                node.left = None
            else:
                self.deleteAllLeaves( childL )

        if childR != None:
            if childR.left == childR.right == None: # childR is a leaf
                node.right = None
            else:
                self.deleteAllLeaves( childR )

    # this variant returns true from a leaf and false from a non-leaf
    # it simplifies checking the left and the right child
    def deleteAllLeaves2(self, node):
        childL = node.left
        childR = node.right

        if childL.left == childL.right == None:
            return True # node is a leaf

        # deletion happens in the next two if statements
        if self.deleteAllLeaves( childL ) == True:
            node.left = None

        if self.deleteAllLeaves( childR ) == True:
            node.right = None

        return False # this (was) not a leaf

    # this method supposes that the tree root has two children
    # add another method which will also correctly manipulate
    # a tree which root has just one child
    # ---------------
    # PostOrder scheme:
    # When a node Y has 1 child X the function returns the reference to X
    # and the parent of Y automatically deletes Y by connecting itself to  X directly
    # when the recursion returns to the parent of Y.
    # When a node has 0 or 2 children the reference to node itself is returned -- parent has nothing to do.
    def removeNodesWith1Child(self, node):
        if node == None: return None

        node.left = self.removeNodesWith1Child( node.left)
        node.right = self.removeNodesWith1Child( node.right)

        if node.left != None and node.right == None:
            return node.left
        if node.right != None and node.left == None:
            return node.right

        return node  # otherwise

    # mirror image of the original tree
    def flipTree(self, node):
        if node == None: return None
        LsubTree = self.flipTree( node.left )
        RsubTree = self.flipTree( node.right )
        node.left, node.right = RsubTree, LsubTree
        return node

    # when a node is a leaf with key K the function creates its two new children
    # the key of the left child is K-1 and the key of the right child is K+1
    def splitLeaves(self, node):
        if node == None: return
        if node.left == None and node.right == None:
            node.left = Node( node.key-1 )
            node.right = Node( node.key+1 )
            return
        self.splitLeaves( node.left )
        self.splitLeaves( node.right )


    # builds a complete balanced tree with the given depth
    # (the total number of nodes in this tree is 2^(depth+1) - 1

    def buildComplete(self, depth ):
        if depth < 0: return None
        node = Node( random.randrange(10,99) ) # some random key
        node.left = self.buildComplete( depth-1 )
        node.right = self.buildComplete( depth-1 )
        return node


    # removes from the tree the entire right subtree
    # of each node in the given depth
    def cutOffRbranch(self, node, depth ):
        if node == None: return

        if depth == 0:
            node.right = None
            return

        self.cutOffRbranch( node.left, depth-1 )
        self.cutOffRbranch( node.right, depth -1 )


    # =================================
    #    End of class BinaryTree
    # =================================


# ............................................................................
#                M A I N   P R O G R A M
# ............................................................................

t = BinaryTree( )

print( "Random tree" )
t.randomTree( t.root, 3 )  # 2nd param is maximum depth of the tree

print( "Display ")
t.display()

# example function calls

print( "Set node keys to be depths")
t.keysToDepths(t.root, 0)
t.display()

print( "Set node keys to be heights")
t.keysToHeights(t.root)
t.display()

print( "Insert one leftmost node 11")
t.insertLeftmost(t.root, 11)
t.display()

print( "Delete all leaves ")
t.deleteAllLeaves(t.root)
t.display()

print( "Remove all nodes with 1 child  ")
t.removeNodesWith1Child(t.root)
t.display()

print( "Mirror the tree")
t.flipTree(t.root)
t.display()

print( "Create a regular perfectly balanced tree of given depth")
t.root = t.buildComplete(4)
t.display()

print( "Cut off each R branch in a given depth")
t.cutOffRbranch(t.root, 2)
t.display()




























