ScopedList.java

/*
** Module   : ScopedList.java
** Abstract : Implementation of a scoped list, biased toward high performance iteration
**
** Copyright (c) 2016-2023, Golden Code Development Corporation.
**
** -#- -I- --Date-- ------------------------------------------Description-------------------------------------
** 001 ECF 20160320 Created initial version to replace ScopedDictionary for use cases where fast
**                  iteration of elements is needed and lookup by key is not.
** 002 ECF 20190319 Added command line test harness.
**     CA  20190319 Fixed the tail element adjustment for the ScopedList instance, when adding an
**                  element as last for a scope depth other than 0.
** 003 CA  20190911 Fixed tail/head adjustment when removing an element.
** 004 CA  20221006 In NodeList.remove, instead of looking through the entire linked list, use a map to 
**                  identify the element's Node instance; if it exists, then remove can be completed.
**                  This is required because for dynamic buffers, the removed element is random. Refs #6821
** 005 GBB 20230512 Logging methods replaced by CentralLogger/ConversionStatus.
** 006 CA  20231129 Small changes to avoid 'invokeinterface' in the compiled bytecode.
*/
/*
** This program is free software: you can redistribute it and/or modify
** it under the terms of the GNU Affero General Public License as
** published by the Free Software Foundation, either version 3 of the
** License, or (at your option) any later version.
**
** This program is distributed in the hope that it will be useful,
** but WITHOUT ANY WARRANTY; without even the implied warranty of
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
** GNU Affero General Public License for more details.
**
** You may find a copy of the GNU Affero GPL version 3 at the following
** location: https://www.gnu.org/licenses/agpl-3.0.en.html
** 
** Additional terms under GNU Affero GPL version 3 section 7:
** 
**   Under Section 7 of the GNU Affero GPL version 3, the following additional
**   terms apply to the works covered under the License.  These additional terms
**   are non-permissive additional terms allowed under Section 7 of the GNU
**   Affero GPL version 3 and may not be removed by you.
** 
**   0. Attribution Requirement.
** 
**     You must preserve all legal notices or author attributions in the covered
**     work or Appropriate Legal Notices displayed by works containing the covered
**     work.  You may not remove from the covered work any author or developer
**     credit already included within the covered work.
** 
**   1. No License To Use Trademarks.
** 
**     This license does not grant any license or rights to use the trademarks
**     Golden Code, FWD, any Golden Code or FWD logo, or any other trademarks
**     of Golden Code Development Corporation. You are not authorized to use the
**     name Golden Code, FWD, or the names of any author or contributor, for
**     publicity purposes without written authorization.
** 
**   2. No Misrepresentation of Affiliation.
** 
**     You may not represent yourself as Golden Code Development Corporation or FWD.
** 
**     You may not represent yourself for publicity purposes as associated with
**     Golden Code Development Corporation, FWD, or any author or contributor to
**     the covered work, without written authorization.
** 
**   3. No Misrepresentation of Source or Origin.
** 
**     You may not represent the covered work as solely your work.  All modified
**     versions of the covered work must be marked in a reasonable way to make it
**     clear that the modified work is not originating from Golden Code Development
**     Corporation or FWD.  All modified versions must contain the notices of
**     attribution required in this license.
*/

package com.goldencode.p2j.util;

import com.goldencode.p2j.util.logging.*;

import java.io.*;
import java.util.*;

/**
 * A linked list implementation which is organized into scopes, each representing a sub-list of
 * elements. Scopes are pushed onto and popped from a logical stack. An element can only be added
 * if at least one scope is present. The first scope is designated the global scope. The top
 * scope is the current (most recently added) scope.
 * <p>
 * Elements can be added to any scope, using the {@link #addAt(int, Object)} method. Convenience
 * methods exist to easily add an element to the global scope or to the current scope, and to
 * add multiple elements at once. Elements can be removed from any scope, using the {@link
 * #removeFrom(int, Object)} method. Convenience methods exist to remove multiple elements at
 * once.
 * <p>
 * When a scope is popped, all elements associated with the current scope are removed.
 * <p>
 * A read-only sublist containing all the nodes in a particular scope is available via the {@link
 * #sublistAtScope(int)} method. The number of scopes can be queried with {@link #scopes()}.
 * <p>
 * This class is meant to provide a high-performance implementation of a scoped list which is
 * iterated often, in full, from head to tail.
 * 
 * @author  ecf
 *
 * @param   <E>
 *          Element type.
 */
public class ScopedList<E>
implements Iterable<E>
{
   /** Logger */
   private static final CentralLogger LOG = CentralLogger.get(ScopedList.class);
   
   /** List of scopes, each of which contain zero or more nodes */
   private final ArrayList<NodeList> scopes = new ArrayList<>();
   
   /** Head of the list */
   private Node<E> head = null;
   
   /** Tail of the list */
   private Node<E> tail = null;
   
   /** Number of nodes in the list */
   private int size = 0;
   
   /**
    * Default constructor.
    */
   public ScopedList()
   {
   }
   
   /**
    * Return the number of scopes currently open.
    * 
    * @return  Number of scopes.
    */
   public int scopes()
   {
      return scopes.size();
   }
   
   /**
    * Push a new, empty scope.
    */
   public void pushScope()
   {
      NodeList scope = new NodeList();
      scopes.add(scope);
   }
   
   /**
    * Pop a scope, discarding all the nodes it contains.
    */
   public void popScope()
   {
      int scopeCount = scopes();
      NodeList scope = scopes.remove(scopeCount - 1);
      size -= scope.size;
      
      if (size == 0)
      {
         head = null;
      }
      
      // temporarily set tail to null, until we can fix it up below
      tail = null;
      
      if (scopeCount > 1)
      {
         // walk backward through the scopes until we find one with a non-null tail; this
         // becomes our new tail
         for (int i = scopeCount - 2; tail == null && i >= 0; i--)
         {
            NodeList prevScope = scopes.get(i);
            if (prevScope.tail != null)
            {
               tail = prevScope.tail;
               prevScope.tail.next = null;
            }
         }
      }
   }
   
   /**
    * Move all nodes from the current scope into the immediately enclosing scope. The current
    * scope is left behind and must still be removed with a call to {@link #popScope()}, but it
    * will be empty. If there are no elements or there is no enclosing scope, this method is a
    * no-op.
    */
   public void rollUp()
   {
      NodeList sublist = null;
      
      if (scopes.size() <= 1 || (sublist = getScopeAtDepth(0)).isEmpty())
      {
         return;
      }
      
      // TODO: implement a more efficient algorithm than individual remove/add;
      // entire sublist should be moved by just fixing up heads and tails, but there are a lot
      // of edge cases to get right
      for (E elem : sublist)
      {
         removeFrom(0, elem);
         addAt(1, elem);
      }
   }
   
   /**
    * Add the given element to the tail of the current scope (also the tail of the list), or to
    * the tail of the global (first) scope.
    * 
    * @param   global
    *          {@code true} to add the element to the tail of the global scope;
    *          {@code false} to add the element to the tail of the full list.
    * @param   element
    *          Element to be added.
    * 
    * @throws  NoSuchElementException
    *          if there is no scope in the list.
    */
   public void add(boolean global, E element)
   {
      int depth = (global ? scopes() - 1 : 0);
      addAt(depth, element);
   }
   
   /**
    * Add the given element to the tail of the scope at the given scope depth, where zero depth
    * represents the current scope, and a positive depth indicate older scopes. A depth of
    * {@code scopes() - 1} indicates the global scope.
    * 
    * @param   depth
    *          Depth of the scope to which to add the element, as described above.
    * @param   element
    *          Element to be added.
    * 
    * @throws  NoSuchElementException
    *          if there is no scope in the list.
    * @throws  IndexOutOfBoundsException
    *          if {@code depth < 0 || depth >= scopes()}.
    */
   public void addAt(int depth, E element)
   {
      NodeList scope = getScopeAtDepth(depth);
      Node<E> n = new Node<>(element);
      
      if (size == 0)
      {
         head = n;
         tail = n;
      }
      else if (depth == 0)
      {
         // link to previous tail and set new tail to new node
         n.previous = tail;
         if (tail != null)
         {
            tail.next = n;
         }
         
         tail = n;
      }
      else
      {
         int scopeCount = scopes();
         
         // walk back through scopes from the specified depth until we find a tail with which we
         // can cross-link
         for (int i = scopeCount - depth - 1; n.previous == null && i >= 0; i--)
         {
            NodeList s = scopes.get(i);
            Node<E> scopeTail = s.tail;
            if (scopeTail != null)
            {
               n.previous = scopeTail;
               n.next = scopeTail.next;
               if (n.next != null)
               {
                  n.next.previous = n;
               }
               scopeTail.next = n;
            }
         }
         
         // if we found no scope tail, the new node is the new head of the scoped list
         if (n.previous == null)
         {
            n.next = head;
            if (head != null)
            {
               head.previous = n;
            }
            head = n;
         }
         
         // check if this is the new tail element for the entire scoped list
         if (n.next == null)
         {
            n.previous = tail;
            if (tail != null)
            {
               tail.next = n;
            }
            tail = n;
         }
      }
      
      size++;
      
      scope.addNode(n);
   }
   
   /**
    * Add all of the given elements, in their current order, to the tail of the current scope
    * (also the tail of the list), or to the tail of the global (first) scope.
    * 
    * @param   global
    *          {@code true} to add the elements to the tail of the global scope;
    *          {@code false} to add the elements to the tail of the full list.
    * @param   elements
    *          Elements to be added.
    * 
    * @throws  NoSuchElementException
    *          if there is no scope in the list.
    */
   public void addAll(boolean global, List<E> elements)
   {
      int depth = (global ? scopes() - 1 : 0);
      for (E e : elements)
      {
         addAt(depth, e);
      }
   }
   
   /**
    * Remove the given element from the scope at the given depth, where zero depth represents
    * the current scope, and a positive depth indicate older scopes. A depth of {@code scopes()
    * - 1} indicates the global scope.
    * 
    * @param   depth
    *          Depth of the scope from which to remove the element, as described above.
    * @param   element
    *          Element to be removed.
    * 
    * @return  {@code true} if the element was removed;
    *          {@code false} if the element was not found.
    */
   public boolean removeFrom(int depth, E element)
   {
      NodeList scope = getScopeAtDepth(depth);
      if (scope.removeElement(element))
      {
         size--;
         
         return true;
      }
      
      return false;
   }
   
   /**
    * Remove all of the given elements from the current scope, or from the global (first) scope.
    * 
    * @param   global
    *          {@code true} to remove the elements from the global scope;
    *          {@code false} to remove the elements from the full list.
    * @param   elements
    *          Elements to be removed.
    * 
    * @return  The number of elements removed.
    */
   public int removeAllFrom(boolean global, List<E> elements)
   {
      int scopeCount = scopes();
      NodeList scope = (global ? scopes.get(0) : scopes.get(scopeCount - 1));
      int removeCount = scope.removeAllElements(elements);
      size -= removeCount;
      
      if (size == 0)
      {
         head = null;
         tail = null;
      }
      
      return removeCount;
   }
   
   /**
    * Get an unmodifiable list view of the scope at the given depth, where zero depth represents
    * the current scope, and a positive depth indicate older scopes. A depth of {@code scopes()
    * - 1} indicates the global scope.
    * 
    * @param   depth
    *          Scope depth as described above.
    * 
    * @return  A list view of the scope. It can be inspected and iterated, but not modified.
    *          Any methods which would alter the list will throw {@code
    *          UnsupportedOperationException}.
    * 
    * @throws  NoSuchElementException
    *          if there is no scope in the list.
    * @throws  IndexOutOfBoundsException
    *          if {@code depth < 0 || depth >= scopes()}.
    */
   public List<E> sublistAtScope(int depth)
   {
      return getScopeAtDepth(depth);
   }
   
   /**
    * Obtain a read-only iterator on the full list of elements, which iterates from head to tail.
    * 
    * @return  A read-only iterator.
    */
   public Iterator<E> iterator()
   {
      return new Iterator<E>()
      {
         private Node<E> current = head;
         
         @Override
         public boolean hasNext()
         {
            return (current != null);
         }
         
         @Override
         public E next()
         {
            if (current == null)
            {
               throw new NoSuchElementException();
            }
            
            E value = current.value;
            current = current.next;
            
            return value;
         }
      };
   }
   
   /**
    * Get a string representation of the contents of the list, primarily for debugging purposes.
    * Note that only the elements are represented (the scopes are not demarcated) in this
    * implementation.
    * 
    * @return  String representation of the list.
    */
   @Override
   public String toString()
   {
      StringBuilder buf = new StringBuilder("[");
      int i = 0;
      for (E e : this)
      {
         if (i++ > 0)
         {
            buf.append(", ");
         }
         buf.append(e);
      }
      buf.append(']');
      
      return buf.toString();
   }
   
   /**
    * Get the node list representing the scope at the given depth, where zero depth represents
    * the current scope, and a positive depth indicate older scopes. A depth of {@code scopes()
    * - 1} indicates the global scope.
    * 
    * @param   depth
    *          Scope depth as described above.
    * 
    * @return  The node list at the given depth.
    * 
    * @throws  NoSuchElementException
    *          if there is no scope in the list.
    * @throws  IndexOutOfBoundsException
    *          if {@code depth < 0 || depth >= scopes()}.
    */
   private NodeList getScopeAtDepth(int depth)
   {
      if (scopes.isEmpty())
      {
         throw new NoSuchElementException();
      }
      
      return scopes.get(scopes.size() - 1 - depth);
   }
   
   /**
    * A node in the list. Contains an element and points to its predecessor and successor nodes
    * in the list (if any).
    * 
    * @param   <E>
    *          Element type.
    */
   private static class Node<E>
   {
      /** Contained element */
      final E value;
      
      /** Previous node */
      Node<E> previous = null;
      
      /** Next node */
      Node<E> next = null;
      
      /**
       * Constructor.
       * 
       * @param   value
       *          Contained element.
       */
      Node(E value)
      {
         this.value = value;
      }
      
      /**
       * Get a string representation of the node, primarily for debugging purposes. Describes the
       * node's contained element, as well as the elements contained by the node's surrounding
       * nodes.
       * 
       * @return  String representation of the node.
       */
      @Override
      public String toString()
      {
         StringBuilder buf = new StringBuilder();
         buf.append(previous != null ? previous.value : null);
         buf.append(" <- ");
         buf.append(value);
         buf.append(" -> ");
         buf.append(next != null ? next.value : null);
         
         return buf.toString();
      }
   }
   
   /**
    * A linked list of nodes representing a single scope in the containing, scoped list. This
    * class implements the {@code List} interface to provide a read-only view of this scope's
    * elements.
    * <p>
    * A private API is provided to allow add and removal operations by the enclosing class.
    */
   private class NodeList
   extends AbstractSequentialList<E>
   {
      /** Head of the list */
      private Node<E> head = null;
      
      /** Tail of the list */
      private Node<E> tail = null;
      
      /** Size of the list */
      private int size = 0;
      
      /** 
       * Keep all nodes' values in an identity map - this allows resolution of the {@link Node}  the value is
       * kept.
       */
      private IdentityHashMap<E, Node<E>> valueToNodes = new IdentityHashMap<>();
      
      /**
       * Default constructor.
       */
      NodeList()
      {
      }
      
      /**
       * Obtain a read-only list iterator on this scope's elements. Methods which modify the list
       * throw {@code UnsupportedOperationException}.
       * 
       * @param   cursor
       *          Zero-based cursor position at which to begin the iteration.
       * 
       * @return  Read-only list iterator.
       * 
       * @throws  IndexOutOfBoundsException
       *          if {@code cursor < 0 || cursor > size()}
       */
      @Override
      public ListIterator<E> listIterator(int cursor)
      {
         return new Iter(cursor);
      }
      
      /**
       * Get the number of elements in this list.
       * 
       * @return  Size of the list.
       */
      @Override
      public int size()
      {
         return size;
      }
      
      /**
       * Add the given node to the tail of the list. This operation only affects the state of
       * this node list (head, tail, and size), but does not change the state of the node
       * itself. The node is cross-linked to surrounding nodes by the enclosing class.
       * 
       * @param   node
       *          Node to be added.
       */
      private void addNode(Node<E> node)
      {
         // if new node is first node, set it as head of the list
         if (head == null)
         {
            head = node;
         }
         
         // set new node as new tail; caller will cross-link nodes
         tail = node;
         
         valueToNodes.put(node.value, node);

         size++;
      }
      
      /**
       * Remove the node which contains the given element, if any.
       * <p>
       * This operation affects both the state of this node list (head, tail, size), the state of
       * the enclosing list (head and tail, but not size), the state of the given node, and the
       * state of surrounding nodes. If the element is found, its node's links to other nodes are
       * broken, and those nodes' links are fixed up to reference one another.
       * 
       * @param   element
       *          Element whose containing node is to be removed from the list.
       * 
       * @return  {@code true} if the element was removed;
       *          {@code false} if the element was not found.
       */
      private boolean removeElement(E element)
      {
         return (removeElement(head, tail, element) != null);
      }
      
      /**
       * Remove all nodes which match the elements in the given list, using an algorithm that is
       * biased toward an assumption that the given {@code elements} are in the same order as the
       * nodes in the node list.
       * <p>
       * For each node removed, this operation affects both the state of this node list (head,
       * tail, size), the state of the enclosing list (head and tail, but not size), the state of
       * the given node, and the state of surrounding nodes. If an element is found, its node's
       * links to other nodes are broken, and those nodes' links are fixed up to reference one
       * another.
       * 
       * @param   elements
       *          Elements to be removed from the list.
       * 
       * @return  The number of elements removed from this node list.
       */
      private int removeAllElements(List<E> elements)
      {
         int count = 0;
         Node<E> removed = null;
         
         for (E e : elements)
         {
            if (removed == null)
            {
               // single pass from head to tail
               removed = removeElement(head, tail, e);
               if (removed != null)
               {
                  count++;
               }
            }
            else
            {
               Node<E> start = removed.next;
               Node<E> end = removed.previous;
               
               if (start != null)
               {
                  // scan from the node after the last removed node to the tail
                  removed = removeElement(start, tail, e);
                  if (removed != null)
                  {
                     count++;
                  }
               }
               
               if (end != null && removed == null)
               {
                  // scan from the head to the node before the last removed node
                  removed = removeElement(head, end, e);
                  if (removed != null)
                  {
                     count++;
                  }
               }
            }
            
            if (head == null)
            {
               // can't remove any more; node list is empty
               break;
            }
         }
         
         return count;
      }
      
      /**
       * Remove the node which contains the given element, if any. The search for {@code element}
       * is conducted by scanning forward from the given {@code start} node to the given {@code
       * end} node.
       * <p>
       * This operation affects both the state of this node list (head, tail, size), the state of
       * the enclosing list (head and tail, but not size), the state of the given node, and the
       * state of surrounding nodes. If the element is found, its node's links to other nodes are
       * broken, and those nodes' links are fixed up to reference one another.
       * 
       * @param   begin
       *          Node at which to start scanning for the given element.
       * @param   end
       *          Node at which to stop scanning for the given element.
       * @param   element
       *          Element whose containing node is to be removed from the list.
       * 
       * @return  {@code true} if the element was removed;
       *          {@code false} if the element was not found.
       */
      private Node<E> removeElement(Node<E> begin, Node<E> end, E element)
      {
         Node<E> current = valueToNodes.get(element);
         if (current == null)
         {
            return null;
         }
         
         Node<E> previous = current.previous;
         Node<E> next = current.next;
         
         // fix up enclosing ScopeList's head and tail, if necessary
         if (current == ScopedList.this.head)
         {
            ScopedList.this.head = next;
         }
         if (current == ScopedList.this.tail)
         {
            ScopedList.this.tail = previous;
         }
         
         // fix up this scope's head and tail, if necessary
         Node<E> newHead = this.head;
         Node<E> newTail = this.tail;
         if (current == this.head)
         {
            newHead = (current != this.tail ? next : null);
         }
         if (current == this.tail)
         {
            newTail = (current != this.head ? previous : null);
         }
         this.head = newHead;
         this.tail = newTail;
         
         // remove this node and cross-link the previous and next nodes to each other
         if (previous != null)
         {
            previous.next = next;
         }
         if (next != null)
         {
            next.previous = previous;
         }
         
         size--;
         
         valueToNodes.remove(current.value);
         
         return current;
      }
      
      /**
       * Implementation of a read-only list iterator for a node list. Reads only within the scope
       * represented by the node list; nodes attached to the head and tail nodes, which are
       * outside this scope, are ignored.
       */
      private class Iter
      implements ListIterator<E>
      {
         /** Node currently being iterated */
         private Node<E> current = head;
         
         /** Iteration cursor position */
         private int index;
         
         /**
          * Constructor which accepts an initial cursor position.
          * 
          * @param   index
          *          Zero-based cursor position. The logical cursor is positioned before the
          *          element at this index.
          * 
          * @throws  IndexOutOfBoundsException
          *          if {@code index < 0 || index > size()}
          */
         Iter(int index)
         {
            if (index < 0 || index > size)
            {
               throw new IndexOutOfBoundsException("index = " + index + "; size = " + size);
            }
            
            for (int i = 0; i < index; i++)
            {
               current = current.next;
            }
            
            this.index = index;
         }
         
         /**
          * Indicate whether there is a next element to visit.
          * 
          * @return  {@code true} if there is a next element, else {@code false}.
          */
         @Override
         public boolean hasNext()
         {
            return (current != null);
         }
         
         /**
          * Return the element immediately after the current cursor position and move the cursor
          * forward one element.
          * 
          * @throws  NoSuchElementException
          *          if there is no available element to return.
          */
         @Override
         public E next()
         {
            if (current == null)
            {
               throw new NoSuchElementException();
            }
            
            E value = current.value;
            current = (current != tail ? current.next : null);
            index++;
            
            return value;
         }
         
         /**
          * Indicate whether there is a previous element to visit.
          * 
          * @return  {@code true} if there is a previous element, else {@code false}.
          */
         @Override
         public boolean hasPrevious()
         {
            return (current != null);
         }
         
         /**
          * Return the element immediately before the current cursor position and move the cursor
          * backward one element.
          * 
          * @throws  NoSuchElementException
          *          if there is no available element to return.
          */
         @Override
         public E previous()
         {
            if (current == null)
            {
               throw new NoSuchElementException();
            }
            
            E value = current.value;
            current = (current != head ? current.previous : null);
            index--;
            
            return value;
         }
         
         /**
          * Report the index of the element immediately after the cursor's current position.
          * Note that this may report an invalid index beyond the last element.
          * 
          * @return  Next element's index.
          */
         @Override
         public int nextIndex()
         {
            return index;
         }
         
         /**
          * Report the index of the element immediately before the cursor's current position.
          * Note that this may report an invalid index beyond the first element.
          * 
          * @return  Next element's index.
          */
         @Override
         public int previousIndex()
         {
            return index - 1;
         }
         
         /**
          * Remove the current element -- unsupported in this read-only implementation.
          * 
          * @throws  UnsupportedOperationException
          *          always.
          */
         @Override
         public void remove()
         {
            throw new UnsupportedOperationException();
         }
         
         /**
          * Replace the current element -- unsupported in this read-only implementation.
          * 
          * @throws  UnsupportedOperationException
          *          always.
          */
         @Override
         public void set(E e)
         {
            throw new UnsupportedOperationException();
         }
         
         /**
          * Add a new element after the current element -- unsupported in this read-only
          * implementation.
          * 
          * @throws  UnsupportedOperationException
          *          always.
          */
         @Override
         public void add(E e)
         {
            throw new UnsupportedOperationException();
         }
      }
   }
   
   /**
    * Simple, command line, test harness.
    * <p>
    * Commands:
    * <ul>
    * <li>"v" to push a scope</li>
    * <li>"^" to pop a scope</li>
    * <li>"a" to add an element</li>
    * <li>"r" to remove an element</li>
    * <li>"q" to quit</li>
    * </ul>
    * 
    * @param   args
    *          None.
    */
   public static void main(String[] args)
   {
      ScopedList<Integer> list = new ScopedList<>();
      
      try (InputStreamReader reader = new InputStreamReader(System.in))
      {
         int next = 0;
         
         outer:
         while (true)
         {
            System.out.print("Command (v, ^, a, r, q): ");
            
            switch (read(reader))
            {
               case "v":
                  list.pushScope();
                  System.out.println("Pushed scope");
                  break;
               case "^":
                  if (list.scopes() == 0)
                  {
                     System.out.println("No scope to be popped");
                     continue;
                  }
                  list.popScope();
                  System.out.println("Popped scope");
                  break;
               case "a":
                  if (list.scopes() == 0)
                  {
                     System.out.println("Must add a scope before an element; try again");
                     continue;
                  }
                  System.out.print("Add: global? (y, n): ");
                  boolean global = false;
                  switch (read(reader))
                  {
                     case "y":
                        global = true;
                        break;
                  }
                  list.add(global, ++next);
                  System.out.println("Added element");
                  break;
               case "r":
                  if (list.scopes() == 0)
                  {
                     System.out.println("Must add a scope/element before removing; try again");
                     continue;
                  }
                  int depth, element;
                  while (true)
                  {
                     System.out.print("Remove: depth? (0-" + (list.scopes() - 1) + ") ");
                     try
                     {
                        depth = Integer.parseInt(read(reader));
                     }
                     catch (NumberFormatException exc)
                     {
                        System.out.println("Expected a number; try again");
                        continue;
                     }
                     if (depth < 0 || depth > list.scopes() - 1)
                     {
                        System.out.println("Try again");
                        continue;
                     }
                     List<Integer> sub = list.sublistAtScope(depth);
                     if (sub.isEmpty())
                     {
                        System.out.println("Nothing to remove at that depth; try again");
                        continue outer;
                     }
                     System.out.print("Remove: element? " + sub + " ");
                     try
                     {
                        element = Integer.parseInt(read(reader));
                     }
                     catch (NumberFormatException exc)
                     {
                        System.out.println("Expected a number; try again");
                        continue;
                     }
                     if (!sub.contains(element))
                     {
                        System.out.println("Try again");
                        continue;
                     }
                     break;
                  }
                  if (list.removeFrom(depth, element))
                  {
                     System.out.println("Removed element");
                  }
                  else
                  {
                     System.out.println("Not found");
                  }
                  break;
               case "q":
                  return;
               default:
                  System.out.println("Try again");
                  continue;
            }
            
            int scopes = list.scopes();
            System.out.println("List: " + scopes + " " + list);
            for (int i = 0; i < scopes; i++)
            {
               System.out.println("Depth " + i + ": " + list.sublistAtScope(i));
            }
         }
      }
      catch (Exception exc)
      {
         LOG.severe("", exc);
      }
   }
   
   /**
    * Read a string from the given stream reader.
    * 
    * @param   reader
    *          A stream reader.
    * 
    * @return  The string from the reader.
    * 
    * @throws  IOException
    *          if there is an error reading.
    */
   private static String read(InputStreamReader reader)
   throws IOException
   {
      char[] buf = new char[16];
      int len = reader.read(buf);
      return new String(buf, 0, len).trim();
   }
}