public class BSTUnboundedEnv extends SquareEnvironment
{
  private TreeNode root; // root of the BST
  private int numObj; // stores the number of objects

  // constructors

  public BSTUnboundedEnv()
  {
    // Construct and initialize inherited attributes.
    super();

    root = null;
    numObj = 0;
  }

  // accessor methods

  public int numRows()
  {
    return -1;
  }

  public int numCols()
  {
    return -1;
  }

  public boolean isValid(Location loc)
  {
    return loc != null;
  }

  public int numObjects()
  {
    return numObj;
  }

  public Locatable[] allObjects()
  {
    Locatable[] objectArray = new Locatable[numObjects()];

    // Put all the environment objects in the list.
    // Uses inorder traversal with allObjectsHelper method.
    allObjectsHelper(root, objectArray, 0);
    return objectArray;
  }

  private int allObjectsHelper(TreeNode node,
                               Locatable[] objectArray, int indexIn)
  {
    if ( node != null )
    {
      int index = allObjectsHelper(node.getLeft(),
                                   objectArray, indexIn);

      objectArray[index] = (Locatable)node.getValue();
      index++;

      return allObjectsHelper(node.getRight(), objectArray, index);
    }
    else
    {
      return indexIn;
    }
  }

  public boolean isEmpty(Location loc)
  {
    return (objectAt(loc) == null);
  }

  public Locatable objectAt(Location loc)
  {
    return objectAtHelper(root, loc);
  }

  private Locatable objectAtHelper(TreeNode node, Location loc)
  {
    if ( node == null )
      return null;
    else
    {
      int cmp =
          loc.compareTo(((Locatable)node.getValue()).location());
      if (cmp == 0)
        return (Locatable)node.getValue();
      else if ( cmp < 0 )
        return objectAtHelper(node.getLeft(), loc);
      else // cmp > 0
        return objectAtHelper(node.getRight(), loc);
    }
  }

  public String toString()
  {
    Locatable[] theObjects = allObjects();
    String s = "Environment contains " + numObjects() + " objects: ";
    for ( int index = 0; index < theObjects.length; index++ )
      s += theObjects[index].toString() + " ";
    return s;
  }

  // modifier methods

  public void add(Locatable obj)
  {
    if ( !isEmpty(obj.location()) )
      throw new IllegalArgumentException("Location " + obj.location() +
                                      " is not a valid empty location");
    root = addHelper(root, obj);
  }

  private TreeNode addHelper(TreeNode node, Locatable obj)
  {
    if ( node == null )
    {
      numObj++;
      return new TreeNode(obj);
    }

    Location loc = obj.location();
    if ( loc.compareTo(((Locatable)node.getValue()).location()) < 0 )
      node.setLeft(addHelper(node.getLeft(), obj));
    else //loc.compareTo(((Locatable)node.getValue()).location()) > 0
      node.setRight(addHelper(node.getRight(), obj));

    return node;
  }

  public void remove(Locatable obj)
  {
    root = removeHelper(root, obj, obj.location());
  }

  private TreeNode removeHelper(TreeNode node,
                                Locatable obj, Location loc)
  {
    if ( node == null )
    {
      throw new IllegalArgumentException("Cannot remove " +
                                          obj + "; not there");
    }
    else if ( node.getValue().equals(obj) ) // found it
    {
      numObj--;
      return removeTargetNode(node);
    }
    else if ( ((Locatable)node.getValue()).location().equals(loc) )
    {
      throw new IllegalArgumentException("Cannot remove " + obj +
                                  "; different object at its location");
    }
    else if (loc.compareTo(((Locatable)node.getValue()).location()) < 0)
    {
      node.setLeft(removeHelper(node.getLeft(), obj, loc));
      return node;
    }
    else //loc.compareTo(((Locatable)node.getValue()).location()) > 0
    {
      node.setRight(removeHelper(node.getRight(), obj, loc));
      return node;
    }
  }

  private TreeNode removeTargetNode(TreeNode target)
  {
    if ( target.getRight() == null )
    {
      return target.getLeft();
    }
    else if ( target.getLeft() == null )
    {
      return target.getRight();
    }
    else if ( target.getRight().getLeft() == null )
    {
      target.setValue(target.getRight().getValue());
      target.setRight(target.getRight().getRight());
      return target;
    }
    else // right child has left child
    {
      TreeNode parent = target.getRight();
      while ( parent.getLeft().getLeft() != null )
        parent = parent.getLeft();

      target.setValue(parent.getLeft().getValue());
      parent.setLeft(parent.getLeft().getRight());
      return target;
    }
  }

  public void recordMove(Locatable obj, Location oldLoc)
  {
    Location newLoc = obj.location();
    int oldCount = numObj;

    root = removeHelper(root, obj, oldLoc);

    if ( oldCount == numObj || !isEmpty(newLoc) )
    {
      throw new
        IllegalArgumentException("Precondition violation moving "
                                  + obj + " from " + oldLoc);
    }

    root = addHelper(root, obj);
  }
}


