"""
B-tree is used to disk operations. Each node (except root) contains
at least t-1 keys (t children) and at most 2*t - 1 keys (2*t children)
where t is the degree of b-tree. It is not a kind of typical bst tree, because
this tree grows up.
B-tree is balanced which means that the difference between height of left
subtree and right subtree is at most 1.

Complexity
    n - number of elements
    t - degree of tree
    Tree always has height at most logt (n+1)/2
    Algorithm        Average        Worst case
    Space            O(n)           O(n)
    Search           O(log n)       O(log n)
    Insert           O(log n)       O(log n)
    Delete           O(log n)       O(log n)
"""


class Node:
    """ Class of Node"""

    def __init__(self):
        # self.is_leaf = is_leaf
        self.keys = []
        self.children = []

    def __repr__(self):
        return "<id_node: {0}>".format(self.keys)

    @property
    def is_leaf(self):
        """ Return if it is a leaf"""
        return len(self.children) == 0


class BTree:
    """ Class of BTree """

    def __init__(self, t_val=2):
        self.min_numbers_of_keys = t_val - 1
        self.max_number_of_keys = 2 * t_val - 1

        self.root = Node()

    def _split_child(self, parent: Node, child_index: int):
        new_right_child = Node()
        half_max = self.max_number_of_keys // 2
        child = parent.children[child_index]
        middle_key = child.keys[half_max]
        new_right_child.keys = child.keys[half_max + 1:]
        child.keys = child.keys[:half_max]
        # child is left child of parent after splitting

        if not child.is_leaf:
            new_right_child.children = child.children[half_max + 1:]
            child.children = child.children[:half_max + 1]

        parent.keys.insert(child_index, middle_key)
        parent.children.insert(child_index + 1, new_right_child)

    def insert_key(self, key):
        """ overflow, tree increases in height """
        if len(self.root.keys) >= self.max_number_of_keys:
            new_root = Node()
            new_root.children.append(self.root)
            self.root = new_root
            self._split_child(new_root, 0)
            self._insert_to_nonfull_node(self.root, key)
        else:
            self._insert_to_nonfull_node(self.root, key)

    def _insert_to_nonfull_node(self, node: Node, key):
        i = len(node.keys) - 1
        while i >= 0 and node.keys[i] >= key:  # find position where insert key
            i -= 1

        if node.is_leaf:
            node.keys.insert(i + 1, key)
        else:
            # overflow
            if len(node.children[i + 1].keys) >= self.max_number_of_keys:
                self._split_child(node, i + 1)
                # decide which child is going to have a new key
                if node.keys[i + 1] < key:
                    i += 1

            self._insert_to_nonfull_node(node.children[i + 1], key)

    def find(self, key) -> bool:
        """ Finds key """
        current_node = self.root
        while True:
            i = len(current_node.keys) - 1
            while i >= 0 and current_node.keys[i] > key:
                i -= 1
            if i >= 0 and current_node.keys[i] == key:
                return True
            if current_node.is_leaf:
                return False
            current_node = current_node.children[i + 1]

    def remove_key(self, key):
        self._remove_key(self.root, key)

    def _remove_key(self, node: Node, key) -> bool:
        try:
            key_index = node.keys.index(key)
            if node.is_leaf:
                node.keys.remove(key)
            else:
                self._remove_from_nonleaf_node(node, key_index)
            return True

        except ValueError:  # key not found in node
            if node.is_leaf:
                print("Key not found.")
                return False  # key not found
            else:
                i = 0
                number_of_keys = len(node.keys)
                # decide in which subtree may be key
                while i < number_of_keys and key > node.keys[i]:
                    i += 1

                action_performed = self._repair_tree(node, i)
                if action_performed:
                    return self._remove_key(node, key)
                else:
                    return self._remove_key(node.children[i], key)

    def _repair_tree(self, node: Node, child_index: int) -> bool:
        child = node.children[child_index]
        # The leaf/node is correct
        if self.min_numbers_of_keys < len(child.keys) <= self.max_number_of_keys:
            return False

        if child_index > 0 and len(node.children[child_index - 1].keys) > self.min_numbers_of_keys:
            self._rotate_right(node, child_index)
            return True

        if (child_index < len(node.children) - 1
                and len(node.children[child_index + 1].keys) > self.min_numbers_of_keys):  # 0 <-- 1
            self._rotate_left(node, child_index)
            return True

        if child_index > 0:
            # merge child with brother on the left
            self._merge(node, child_index - 1, child_index)
        else:
            # merge child with brother on the right
            self._merge(node, child_index, child_index + 1)

        return True

    def _rotate_left(self, parent_node: Node, child_index: int):
        """
        Take key from right brother of the child and transfer to the child
        """
        new_child_key = parent_node.keys[child_index]
        new_parent_key = parent_node.children[child_index + 1].keys.pop(0)
        parent_node.children[child_index].keys.append(new_child_key)
        parent_node.keys[child_index] = new_parent_key

        if not parent_node.children[child_index + 1].is_leaf:
            ownerless_child = parent_node.children[child_index
                                                   + 1].children.pop(0)
            # make ownerless_child as a new biggest child (with highest key)
            # -> transfer from right subtree to left subtree
            parent_node.children[child_index].children.append(ownerless_child)

    def _rotate_right(self, parent_node: Node, child_index: int):
        """
        Take key from left brother of the child and transfer to the child
        """
        parent_key = parent_node.keys[child_index - 1]
        new_parent_key = parent_node.children[child_index - 1].keys.pop()
        parent_node.children[child_index].keys.insert(0, parent_key)
        parent_node.keys[child_index - 1] = new_parent_key

        if not parent_node.children[child_index - 1].is_leaf:
            ownerless_child = parent_node.children[child_index
                                                   - 1].children.pop()
            # make ownerless_child as a new lowest child (with lowest key)
            # -> transfer from left subtree to right subtree
            parent_node.children[child_index].children.insert(
                0, ownerless_child)

    def _merge(self, parent_node: Node, to_merge_index: int, transfered_child_index: int):
        from_merge_node = parent_node.children.pop(transfered_child_index)
        parent_key_to_merge = parent_node.keys.pop(to_merge_index)
        to_merge_node = parent_node.children[to_merge_index]
        to_merge_node.keys.append(parent_key_to_merge)
        to_merge_node.keys.extend(from_merge_node.keys)

        if not to_merge_node.is_leaf:
            to_merge_node.children.extend(from_merge_node.children)

        if parent_node == self.root and not parent_node.keys:
            self.root = to_merge_node

    def _remove_from_nonleaf_node(self, node: Node, key_index: int):
        key = node.keys[key_index]
        left_subtree = node.children[key_index]
        if len(left_subtree.keys) > self.min_numbers_of_keys:
            largest_key = self._find_largest_and_delete_in_left_subtree(
                left_subtree)
        elif len(node.children[key_index + 1].keys) > self.min_numbers_of_keys:
            largest_key = self._find_largest_and_delete_in_right_subtree(
                node.children[key_index + 1])
        else:
            self._merge(node, key_index, key_index + 1)
            return self._remove_key(node, key)

        node.keys[key_index] = largest_key

    def _find_largest_and_delete_in_left_subtree(self, node: Node):
        if node.is_leaf:
            return node.keys.pop()
        else:
            ch_index = len(node.children) - 1
            self._repair_tree(node, ch_index)
            largest_key_in_subtree = self._find_largest_and_delete_in_left_subtree(
                node.children[len(node.children) - 1])
            # self._repair_tree(node, ch_index)
            return largest_key_in_subtree

    def _find_largest_and_delete_in_right_subtree(self, node: Node):
        if node.is_leaf:
            return node.keys.pop(0)
        else:
            ch_index = 0
            self._repair_tree(node, ch_index)
            largest_key_in_subtree = self._find_largest_and_delete_in_right_subtree(
                node.children[0])
            # self._repair_tree(node, ch_index)
            return largest_key_in_subtree

    def traverse_tree(self):
        self._traverse_tree(self.root)
        print()

    def _traverse_tree(self, node: Node):
        if node.is_leaf:
            print(node.keys, end=" ")
        else:
            for i, key in enumerate(node.keys):
                self._traverse_tree(node.children[i])
                print(key, end=" ")
            self._traverse_tree(node.children[-1])