/**
   * @param parent: The parent of the element to be removed
   * @param remove: The element to remove from the Tree
   * @param direction: false if remove is to the left of parent, true otherwise
   *     <p>This method physically removes the AVLNode with the element from the AVLTree, replacing
   *     it with the appropriate successor. *
   */
  private void enactRemoval(AVLNode<E> parent, AVLNode<E> remove, boolean direction) {
    AVLNode<E> temp = null;
    AVLNode<E> left = remove.getLeft();
    AVLNode<E> right = remove.getRight();

    // if the Node to remove is not a leaf, find the appropriate successor
    if (left != null || right != null) {
      temp = findSuccessor(remove);
    }

    // if remove is the right child of parent, update parent's right node
    if (direction && (parent != rootAbove)) {
      parent.setRightNode(temp);
    }

    // otherwise, update its left node with the successor
    else parent.setLeftNode(temp);

    // and update temp to point to remove's children
    if (temp != null) {

      if (temp != left) {
        temp.setLeftNode(remove.getLeft());
      }

      if (temp != right) {
        temp.setRightNode(remove.getRight());
      }
    }

    // and finally, discard those references from remove
    // so that the removed Node is garbage collected sooner
    remove.setLeftNode(null);
    remove.setRightNode(null);
  }
  /**
   * @param root: The element for which to find a successor AVLNode
   * @return AVLNode<E>: The successor Node *
   */
  private AVLNode<E> findSuccessor(AVLNode<E> root) {
    AVLNode<E> temp = root;
    AVLNode<E> parent = null;

    // if the balance favors the right, traverse right
    // otherwise, traverse left
    boolean direction = (temp.getBalance() > 0);

    parent = temp;
    temp = (direction) ? temp.getRight() : temp.getLeft();

    if (temp == null) return temp;

    // and find the farthest left-Node on the right side,
    // or the farthest right-Node on the left side
    while ((temp.getRight() != null && !direction) || (temp.getLeft() != null && direction)) {

      parent = temp;
      temp = (direction) ? temp.getLeft() : temp.getRight();
    }

    // finally, update the successor's parent's references
    // to adjust for a left child on the right node, or a right
    // child on the left-node
    if (temp == parent.getLeft()) {
      parent.setLeftNode(temp.getRight());
      temp.setRightNode(null);
    } else {
      parent.setRightNode(temp.getLeft());
      temp.setLeftNode(null);
    }

    return temp;
  }
  /**
   * @param rotateBase: The root of the subtree that is being rotated
   * @param rootAbove: The AVLNode that points to rotateBase
   *     <p>This method rotates the subtree balancing it to within a margin of |1|.
   */
  public void rotate(AVLNode<E> rotateBase, AVLNode<E> rootAbove) {
    int balance = rotateBase.getBalance();

    if (Math.abs(balance) < 2) {
      // System.out.println("No rotate");
    }

    // gets the child on the side with the greater height
    AVLNode<E> child = (balance < 0) ? rotateBase.getLeft() : rotateBase.getRight();

    if (child == null) return;

    int childBalance = child.getBalance();
    AVLNode<E> grandChild = null;

    // both the child and grandchild are on the
    // left side, so rotate the child up to the root position
    if (balance < -1 && childBalance < 0) {
      if (rootAbove != this.rootAbove && rootAbove.getRight() == rotateBase) {
        rootAbove.setRightNode(child);
      } else {
        rootAbove.setLeftNode(child);
      }

      grandChild = child.getRight();
      child.setRightNode(rotateBase);
      rotateBase.setLeftNode(grandChild);
      return;
    }

    // both the child and the grandchild are on the
    // right side, so rotate the child up to the root position
    else if (balance > 1 && childBalance > 0) {
      if (rootAbove != this.rootAbove && rootAbove.getRight() == rotateBase) {
        rootAbove.setRightNode(child);
      } else {
        rootAbove.setLeftNode(child);
      }

      grandChild = child.getLeft();
      child.setLeftNode(rotateBase);
      rotateBase.setRightNode(grandChild);
      return;
    }

    // the child is on the left side, but the grandchild is on the
    // right side, so rotate the grandchild up to the child position
    // so the condition of the first if statement is satisfied,
    // then recurse to have the first if statement evaluated
    else if (balance < -1 && childBalance > 0) {
      grandChild = child.getRight();
      rotateBase.setLeftNode(grandChild);
      child.setRightNode(grandChild.getLeft());
      grandChild.setLeftNode(child);
      rotate(rotateBase, rootAbove);
      return;
    }

    // the child is on the right side, but the grandchild is on the
    // left side, so rotate the grandchild up to the child position
    // so the condition of the second if statement is satisfied,
    // then recurse to have the second if statement evaluated
    else if (balance > 1 && childBalance < 0) {
      grandChild = child.getLeft();
      rotateBase.setRightNode(grandChild);
      child.setLeftNode(grandChild.getRight());
      grandChild.setRightNode(child);
      rotate(rotateBase, rootAbove);
      return;
    }
  }