/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 * 
 *   http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

package org.apache.uima.tools.viewer;

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.ComponentOrientation;
import java.awt.Dimension;
import java.awt.GridLayout;
import java.awt.Insets;
import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.ItemEvent;
import java.awt.event.ItemListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.swing.ButtonGroup;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JComboBox;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JRadioButton;
import javax.swing.JScrollPane;
import javax.swing.JSplitPane;
import javax.swing.JTextPane;
import javax.swing.JTree;
import javax.swing.Scrollable;
import javax.swing.event.TreeExpansionEvent;
import javax.swing.event.TreeExpansionListener;
import javax.swing.event.TreeWillExpandListener;
import javax.swing.text.BadLocationException;
import javax.swing.text.DefaultStyledDocument;
import javax.swing.text.Document;
import javax.swing.text.MutableAttributeSet;
import javax.swing.text.SimpleAttributeSet;
import javax.swing.text.StyleConstants;
import javax.swing.text.StyledDocument;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.DefaultTreeCellRenderer;
import javax.swing.tree.DefaultTreeModel;
import javax.swing.tree.ExpandVetoException;
import javax.swing.tree.TreeNode;
import javax.swing.tree.TreePath;

import org.apache.uima.cas.ArrayFS;
import org.apache.uima.cas.CAS;
import org.apache.uima.cas.CASException;
import org.apache.uima.cas.FSIterator;
import org.apache.uima.cas.Feature;
import org.apache.uima.cas.FeatureStructure;
import org.apache.uima.cas.SofaFS;
import org.apache.uima.cas.Type;
import org.apache.uima.cas.impl.BooleanArrayFSImpl;
import org.apache.uima.cas.impl.ByteArrayFSImpl;
import org.apache.uima.cas.impl.DoubleArrayFSImpl;
import org.apache.uima.cas.impl.FloatArrayFSImpl;
import org.apache.uima.cas.impl.IntArrayFSImpl;
import org.apache.uima.cas.impl.LongArrayFSImpl;
import org.apache.uima.cas.impl.ShortArrayFSImpl;
import org.apache.uima.cas.impl.StringArrayFSImpl;
import org.apache.uima.cas.text.AnnotationFS;
import org.apache.uima.jcas.JCas;
import org.apache.uima.jcas.tcas.Annotation;

/**
 * A Swing component that displays annotations in a text pane with highlighting. There is also a
 * tree view for display details of annotations on which the user clicks. This class extends
 * {@link JPanel} and so can be reused within any Swing application.
 * <p>
 * To launch the viewer, call the {@link #setCAS(CAS)} method with the CAS to be viewed.
 * <p>
 * The viewer is configurable via the following methods:
 * <ul>
 * <li>{@link #setConsistentColors(boolean)} - if set to true (default), the color assigned to any
 * annotation type will be the same across all documents. If set to false, colors may vary across
 * documents.</li>
 * <li>{@link #setDisplayedTypes(String[])} - specifies a set of types that will be highlighted in
 * the viewer's text pane.</li>
 * <li>{@link #setHiddenTypes(String[])} - specifies a set of types that will NOT be highlighted in
 * the viewer's text pane.</li>
 * <li>{@link #setHiddenFeatures(String[])} - specifies a set of features that will never shown in
 * the viewer's annotation details tree.</li>
 * <li>{@link #setHighFrequencyTypes(String[])} - this can be used to specify a set of types that
 * occur frequently. These types will the be assigned the most distinguishable colors.</li>
 * <li>{@link #setInitiallySelectedTypes(String[])} - this can be used to specify a set of types
 * that will initially be selected (i.e. have their checkboxes checked) in the viewer. The default
 * is for all types to be initially selected.</li>
 * <li>{@link #setRightToLeftTextOrientation(boolean)} - switches the text pane from left-to-right
 * (default) to right-to-left mode. This is needed to support languages such as Arabic and Hebrew,
 * which are read from right to left.</li>
 * </ul>
 */
public class CasAnnotationViewer extends JPanel implements ActionListener, MouseListener,
        TreeWillExpandListener, TreeExpansionListener, ItemListener {
  private static final long serialVersionUID = 3559118488371946999L;

  // Mode constants
  private static final short MODE_ANNOTATIONS = 0;

  private static final short MODE_ENTITIES = 1;

  private ArrayList userTypes = null;

  /**
   * @return Returns the userTypes.
   */
  public ArrayList getUserTypes() {
    return userTypes;
  }

  /**
   * @param userTypes
   *          The userTypes to set.
   */
  public void setUserTypes(ArrayList userTypes) {
    this.userTypes = userTypes;
  }

  // colors to use for highlighting annotations
  // (use high brightness for best contrast against black text)
  private static final float BRIGHT = 0.95f;

  private static final Color[] COLORS = new Color[] {
      // low saturation colors are best, so put them first
      Color.getHSBColor(55f / 360, 0.25f, BRIGHT), // butter yellow?
      Color.getHSBColor(0f / 360, 0.25f, BRIGHT), // pink?
      Color.getHSBColor(210f / 360, 0.25f, BRIGHT), // baby blue?
      Color.getHSBColor(120f / 360, 0.25f, BRIGHT), // mint green?
      Color.getHSBColor(290f / 360, 0.25f, BRIGHT), // lavender?
      Color.getHSBColor(30f / 360, 0.25f, BRIGHT), // tangerine?
      Color.getHSBColor(80f / 360, 0.25f, BRIGHT), // celery green?
      Color.getHSBColor(330f / 360, 0.25f, BRIGHT), // light coral?
      Color.getHSBColor(160f / 360, 0.25f, BRIGHT), // aqua?
      Color.getHSBColor(250f / 360, 0.25f, BRIGHT), // light violet?
      // higher saturation colors
      Color.getHSBColor(55f / 360, 0.5f, BRIGHT), Color.getHSBColor(0f / 360, 0.5f, BRIGHT),
      Color.getHSBColor(210f / 360, 0.5f, BRIGHT), Color.getHSBColor(120f / 360, 0.5f, BRIGHT),
      Color.getHSBColor(290f / 360, 0.5f, BRIGHT),
      Color.getHSBColor(30f / 360, 0.5f, BRIGHT),
      Color.getHSBColor(80f / 360, 0.5f, BRIGHT),
      Color.getHSBColor(330f / 360, 0.5f, BRIGHT),
      Color.getHSBColor(160f / 360, 0.5f, BRIGHT),
      Color.getHSBColor(250f / 360, 0.5f, BRIGHT),
      // even higher saturation colors
      Color.getHSBColor(55f / 360, 0.75f, BRIGHT), Color.getHSBColor(0f / 360, 0.75f, BRIGHT),
      Color.getHSBColor(210f / 360, 0.75f, BRIGHT), Color.getHSBColor(120f / 360, 0.75f, BRIGHT),
      Color.getHSBColor(290f / 360, 0.75f, BRIGHT), Color.getHSBColor(30f / 360, 0.75f, BRIGHT),
      Color.getHSBColor(80f / 360, 0.75f, BRIGHT), Color.getHSBColor(330f / 360, 0.75f, BRIGHT),
      Color.getHSBColor(160f / 360, 0.75f, BRIGHT), Color.getHSBColor(250f / 360, 0.75f, BRIGHT) };

  private static String[] DEFAULT_HIDDEN_FEATURES = { "sofa" };

  private Map mTypeNameToColorMap = new HashMap();

  private HashSet noCheckSet = new HashSet();

  private List mHighFrequencyTypes = new ArrayList();

  private Set mDisplayedTypeNames = null;

  private Set mHiddenTypeNames = new HashSet();

  private Set mInitiallySelectedTypeNames = null;

  private Map mTypeToCheckboxMap = new HashMap();

  private Map mEntityToCheckboxMap = new HashMap();

  private CAS mCAS;

  private Type mStringType;

  private Type mFsArrayType;

  private boolean mConsistentColors = true;

  private Set mHiddenFeatureNames = new HashSet();

  private boolean mEntityViewEnabled = false; 

  private short mViewMode = MODE_ANNOTATIONS;

  private boolean mHideUnselectedCheckboxes = false;

  // GUI components
  private JSplitPane horizSplitPane;

  private JSplitPane vertSplitPane;

  private JScrollPane textScrollPane;

  private JTextPane textPane;

  private JPanel legendPanel;

  private JLabel legendLabel;

  private JScrollPane legendScrollPane;

  private JPanel annotationCheckboxPanel;

  private JPanel entityCheckboxPanel;

  private JPanel buttonPanel;

  private JButton selectAllButton;

  private JButton deselectAllButton;

  private JButton showHideUnselectedButton;

  private JTree selectedAnnotationTree;

  private DefaultTreeModel selectedAnnotationTreeModel;

  private JPanel viewModePanel;

  private JRadioButton annotationModeButton;

  private JRadioButton entityModeButton;

  private String[] mBoldfaceKeywords = new String[0];

  private int[] mBoldfaceSpans = new int[0];

  private JPanel sofaSelectionPanel;

  private JComboBox sofaSelectionComboBox;

  private EntityResolver mEntityResolver = new DefaultEntityResolver();

  /**
   * Creates a CAS Annotation Viewer.
   */
  public CasAnnotationViewer() {
    // create a horizonal JSplitPane
    horizSplitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT);
    horizSplitPane.setResizeWeight(0.6);
    this.setLayout(new BorderLayout());
    this.add(horizSplitPane);

    // create a vertical JSplitPane and add to left of horizSplitPane
    vertSplitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT);
    vertSplitPane.setResizeWeight(0.8);
    vertSplitPane.setPreferredSize(new Dimension(620, 600));
    vertSplitPane.setMinimumSize(new Dimension(200, 200));
    horizSplitPane.setLeftComponent(vertSplitPane);

    // add JTextPane to top of vertical split pane
    textPane = new JTextPane();
    textPane.setEditable(false);
    textPane.setPreferredSize(new Dimension(620, 400));
    textPane.setMinimumSize(new Dimension(200, 100));
    textScrollPane = new JScrollPane(textPane);
    vertSplitPane.setTopComponent(textScrollPane);

    // bottom pane is the legend, with checkboxes
    legendPanel = new JPanel();
    legendPanel.setPreferredSize(new Dimension(620, 200));
    legendPanel.setLayout(new BorderLayout());
    legendLabel = new JLabel("Legend");
    legendPanel.add(legendLabel, BorderLayout.NORTH);
    // checkboxes are contained in a scroll pane
    legendScrollPane = new JScrollPane();
    legendScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
    legendPanel.add(legendScrollPane, BorderLayout.CENTER);
    // there are two checkbox panels - one for annotations, one for entities
    annotationCheckboxPanel = new VerticallyScrollablePanel();
    annotationCheckboxPanel.setLayout(new GridLayout(0, 5));
    entityCheckboxPanel = new VerticallyScrollablePanel();
    entityCheckboxPanel.setLayout(new GridLayout(0, 4));
    // add annotation panel first, since that is the default
    legendScrollPane.setViewportView(annotationCheckboxPanel);

    // at very bottom is the Button Panel, with Select All, Deselect All,
    // and Hide/Show Unselected Buttons. It also may show the sofa-selection
    // combo box and/or the Viewer Mode radio group; either are both may
    // be hidden.
    buttonPanel = new JPanel();

    selectAllButton = new JButton("Select All");
    selectAllButton.addActionListener(this);
    buttonPanel.add(selectAllButton);
    deselectAllButton = new JButton("Deselect All");
    deselectAllButton.addActionListener(this);
    buttonPanel.add(deselectAllButton);
    showHideUnselectedButton = new JButton("Hide Unselected");
    showHideUnselectedButton.addActionListener(this);
    buttonPanel.add(showHideUnselectedButton);

    sofaSelectionPanel = new JPanel();
    JLabel sofaSelectionLabel = new JLabel("Sofa:");
    sofaSelectionPanel.add(sofaSelectionLabel);
    sofaSelectionComboBox = new JComboBox();
    sofaSelectionPanel.add(sofaSelectionComboBox);
    sofaSelectionComboBox.addItemListener(this);
    buttonPanel.add(sofaSelectionPanel);

    viewModePanel = new JPanel();
    viewModePanel.add(new JLabel("Mode: "));
    annotationModeButton = new JRadioButton("Annotations");
    annotationModeButton.setSelected(true);
    annotationModeButton.addActionListener(this);
    viewModePanel.add(annotationModeButton);
    entityModeButton = new JRadioButton("Entities");
    entityModeButton.addActionListener(this);
    viewModePanel.add(entityModeButton);
    ButtonGroup group = new ButtonGroup();
    group.add(annotationModeButton);
    group.add(entityModeButton);
    buttonPanel.add(viewModePanel);
    viewModePanel.setVisible(false);
    this.add(buttonPanel, BorderLayout.SOUTH);

    textPane.setMinimumSize(new Dimension(200, 100));
    vertSplitPane.setBottomComponent(legendPanel);

    // right pane has a JTree
    selectedAnnotationTreeModel = new DefaultTreeModel(new DefaultMutableTreeNode("Annotations"));
    selectedAnnotationTree = new JTree(selectedAnnotationTreeModel) {
      private static final long serialVersionUID = -7882967150283952907L;

      public Dimension getPreferredScrollableViewportSize() {
        return new Dimension(230, 500);
      }
    };
    selectedAnnotationTree.setMinimumSize(new Dimension(50, 100));
    selectedAnnotationTree.setScrollsOnExpand(true);
    selectedAnnotationTree.setRootVisible(true);
    selectedAnnotationTree.setCellRenderer(new AnnotationTreeCellRenderer());
    selectedAnnotationTree.addTreeWillExpandListener(this);
    selectedAnnotationTree.addTreeExpansionListener(this);
    JPanel treePanel = new JPanel();
    treePanel.setLayout(new BorderLayout());
    treePanel.add(new JLabel("Click In Text to See Annotation Detail"), BorderLayout.NORTH);

    treePanel.add(new JScrollPane(selectedAnnotationTree), BorderLayout.CENTER);
    horizSplitPane.setRightComponent(treePanel);

    // add mouse listener to update annotation tree
    textPane.addMouseListener(this);

    // initialize hidden feature names map
    mHiddenFeatureNames.addAll(Arrays.asList(DEFAULT_HIDDEN_FEATURES));
  }

  /**
   * @deprecated use the zero-argument constructor and call {@link #setEntityViewEnabled(boolean)}
   */
  @Deprecated
  public CasAnnotationViewer(boolean aEntityViewEnabled) {
    this();
  }

  /**
   * Set the list of types that occur most frequently. This method assigns the most distinguishable
   * colors to these types.
   * 
   * @param aTypeNames
   *          names of types that are occur frequently. Ideally these should be ordered by
   *          frequency, with the most frequent being first.
   */
  public void setHighFrequencyTypes(String[] aTypeNames) {
    // store these types for later
    mHighFrequencyTypes.clear();
    mHighFrequencyTypes.addAll(Arrays.asList(aTypeNames));
    mTypeNameToColorMap.clear();
    assignColors(mHighFrequencyTypes);
  }

  /**
   * Set the list of types that will be highlighted in the viewer. Types not in this list will not
   * appear in the legend and will never be highlighted. If this method is not called, the default
   * is to show all types in the CAS (except those specifically hidden by a call to
   * {@link #setHiddenTypes(String[])}.
   * 
   * @param aDisplayedTypeNames
   *          names of types that are to be highlighted. Null indicates that all types in the CAS
   *          should be highlighted.
   */
  public void setDisplayedTypes(String[] aDisplayedTypeNames) {
    if (aDisplayedTypeNames == null) {
      mDisplayedTypeNames = null;
    } else {
      mDisplayedTypeNames = new HashSet();
      mDisplayedTypeNames.addAll(Arrays.asList(aDisplayedTypeNames));
    }
  }

  /**
   * Set the list of types that will NOT be highlighted in the viewer.
   * 
   * @param aTypeNames
   *          names of types that are never to be highlighted.
   */
  public void setHiddenTypes(String[] aTypeNames) {
    mHiddenTypeNames.clear();
    mHiddenTypeNames.addAll(Arrays.asList(aTypeNames));
  }

  /**
   * Configures the initially selected types in the viewer. If not called, all types will be
   * initially selected.
   * 
   * @param aTypeNames
   *          array of fully-qualified names of types to be initially selected
   */
  public void setInitiallySelectedTypes(String[] aTypeNames) {
    mInitiallySelectedTypeNames = new HashSet();
    for (int i = 0; i < aTypeNames.length; i++) {
      mInitiallySelectedTypeNames.add(aTypeNames[i]);
    }
    // apply to existing checkboxes
    Iterator iterator = mTypeToCheckboxMap.entrySet().iterator();
    while (iterator.hasNext()) {
      Map.Entry entry = (Map.Entry) iterator.next();
      String type = ((Type) entry.getKey()).getName();
      JCheckBox checkbox = (JCheckBox) entry.getValue();
      checkbox.setSelected(typeNamesContains(mInitiallySelectedTypeNames, type));
    }

    // redisplay (if we have a CAS) - this allows this method to be called
    // either before or after displaying the viewer
    if (mCAS != null) {
      display();
    }
  }

  /**
   * Configures the viewer to hide certain features in the annotation deatail pane.
   * 
   * @param aFeatureNames
   *          array of (short) feature names to be hidden
   */
  public void setHiddenFeatures(String[] aFeatureNames) {
    mHiddenFeatureNames.clear();
    // add default hidden features
    mHiddenFeatureNames.addAll(Arrays.asList(DEFAULT_HIDDEN_FEATURES));
    // add user-defined hidden features
    mHiddenFeatureNames.addAll(Arrays.asList(aFeatureNames));
  }

  /**
   * Configures whether the viewer will allow the user to switch to "Entity" view, which highlight
   * entities rather than annotations.  Entity mode is typically only useful if the
   * {@link #setEntityResolver(EntityResolver)} method has been called with a user-supplied
   * class that can determine which annotations refer to the same entity.
   * 
   * @param aEnabled
   *          true to enable entity viewing mode, false to allow annotation viewing only.
   *          The default is false.
   */
  public void setEntityViewEnabled(boolean aEnabled) {
    mEntityViewEnabled = aEnabled;
    this.viewModePanel.setVisible(aEnabled);
  }
  
  /**
   * Sets the {@link EntityResolver} to use when the viewer is in entity mode.
   * Entity mode must be turned on using the {@link #setEntityViewEnabled(boolean)} method.
   * @param aEntityResolver user-supplied class that can determine which annotations correspond
   *   to the same entity.
   */
  public void setEntityResolver(EntityResolver aEntityResolver) {
    mEntityResolver = aEntityResolver;
  }

  /**
   * Sets whether colors will be consistent in all documents viewed using this viewer. If set to
   * true, assignments of color to annotation type will persist across documents; if false, colors
   * will be reassigned in each new document. (Note that if high frequency types are set via
   * {@link #setHighFrequencyTypes(String[])}, the colors for those types will always be
   * consistent, regardless of the value passed to this method.
   * 
   * @param aConsistent
   *          true (the default) causes colors to be consistent across documents, false allows them
   *          to vary
   */
  public void setConsistentColors(boolean aConsistent) {
    mConsistentColors = aConsistent;
  }

  /**
   * Sets the text orientation. The default is left-to-right, but needs to be set to right-to-left
   * to properly display some languages, most notably Arabic and Hebrew.
   * 
   * @param aRightToLeft
   *          true to put the viewer in right-to-left mode, false for left-to-right (the default).
   */
  public void setRightToLeftTextOrientation(boolean aRightToLeft) {
    textPane.applyComponentOrientation(aRightToLeft ? ComponentOrientation.RIGHT_TO_LEFT
            : ComponentOrientation.LEFT_TO_RIGHT);
  }

  /**
   * Sets whether unselected (unchecked) checkboxes will be hidden entirely from the legend. This
   * mode makes for a cleaner legend at the expense of making it more difficult to toggle which
   * types are selected. There's also a button in the GUI that lets the user change this setting.
   * 
   * @param aHideUnselected
   */
  public void setHideUnselectedCheckboxes(boolean aHideUnselected) {
    mHideUnselectedCheckboxes = aHideUnselected;
    display();
  }

  /**
   * Sets the CAS to be viewed. This must be called before {@link #display()}.
   * 
   * @param aCAS
   *          the CSA to be viewed
   */
  public void setCAS(CAS aCAS) {
    mCAS = aCAS;
    mStringType = mCAS.getTypeSystem().getType(CAS.TYPE_NAME_STRING);
    mFsArrayType = mCAS.getTypeSystem().getType(CAS.TYPE_NAME_FS_ARRAY);
    // clear checkbox panel so it will be repopulated
    annotationCheckboxPanel.removeAll();
    entityCheckboxPanel.removeAll();
    mTypeToCheckboxMap.clear();
    mEntityToCheckboxMap.clear();
    // clear selected annotation details tree
    this.updateSelectedAnnotationTree(-1);

    // clear type to color map if color consistency is off
    if (!mConsistentColors) {
      mTypeNameToColorMap.clear();
      // but reassign colors to high frequency types
      assignColors(mHighFrequencyTypes);
    }

    // clear boldface
    mBoldfaceKeywords = new String[0];
    mBoldfaceSpans = new int[0];

    // enable or disable entity view depending on user's choice 
    this.viewModePanel.setVisible(mEntityViewEnabled);

    // Populate sofa combo box with the names of all text Sofas in the CAS
    sofaSelectionComboBox.removeAllItems();
    Iterator sofas = aCAS.getSofaIterator();
    Feature sofaIdFeat = aCAS.getTypeSystem().getFeatureByFullName(CAS.FEATURE_FULL_NAME_SOFAID);
    boolean nonDefaultSofaFound = false;
    while (sofas.hasNext()) {
      SofaFS sofa = (SofaFS) sofas.next();
      if (sofa.getLocalStringData() != null) {
        String sofaId = sofa.getStringValue(sofaIdFeat);
        if (CAS.NAME_DEFAULT_SOFA.equals(sofaId)) {
          sofaId = "DEFAULT"; // make nicer display
        } else {
          nonDefaultSofaFound = true;
        }
        sofaSelectionComboBox.addItem(sofaId);
        // if this sofa matches the view passed to this method, select it
        CAS viewOfSofa = aCAS.getView(sofa);
        if (viewOfSofa == aCAS) {
          sofaSelectionComboBox.setSelectedIndex(sofaSelectionComboBox.getItemCount() - 1);
        }
      }
    }
    if (sofaSelectionComboBox.getItemCount() == 0) {
      throw new RuntimeException("This CAS contains no document to view.");
    }
    // make sofa selector visible if any text sofa other than the default was found
    sofaSelectionPanel.setVisible(nonDefaultSofaFound);

    // Note that selection of the Sofa from the combo box happens during
    // population, and that triggers the call to display() to display
    // that document and its annotations/entities.
    // display();
  }

  /**
   * Causes the specified words to appear in boldface wherever they occur in the document. This is
   * case-insensitive. Call this method after {@link #setCAS(CAS)}. It wil apply only to the current
   * document, and will be reset on the next call to {@link #setCAS(CAS)}.
   * 
   * @param aWords
   *          array of words to highlight in boldface.
   */
  public void applyBoldfaceToKeywords(String[] aWords) {
    mBoldfaceKeywords = aWords;
    doBoldface();
  }

  /**
   * Causes the specified spans to appear in boldface. This is case-insensitive. Call this method
   * after {@link #setCAS(CAS)}. It wil apply only to the current document, and will be reset on the
   * next call to {@link #setCAS(CAS)}.
   * 
   * @param aSpans
   *          spans to appear in boldface (begin1, end1, begin2, end2, ...)
   */
  public void applyBoldfaceToSpans(int[] aSpans) {
    mBoldfaceSpans = aSpans;
    doBoldface();
  }

  /**
   * Configures the viewer appropriately for displaying a hit against an XML fragments query. This
   * does not use a sophisticated algorithm for determining the location of the document that
   * matched the query. Currently all it does is call {@link #setInitiallySelectedTypes(String[])}
   * with the list of types mentioned in the query and {@link #applyBoldfaceToKeywords(String[])} on
   * any keywords mentioned in the query.
   * 
   * @param aQuery
   *          an XML fragments query
   * @param aTypeNamespace
   *          namespace to prepend to the element names in the query in order to form
   *          fully-qualified CAS type names. This is optional; if not specified, type namespaces
   *          are ignored and any type whose local name matches the query will be selected.
   */
  public void configureViewForXmlFragmentsQuery(String aQuery, String aTypeNamespace) {
    // need to parse query and produce type list and keyword list
    List typeList = new ArrayList();
    List keywordList = new ArrayList();

    String delims = "<>+-*\" \t\n";
    StringTokenizer tokenizer = new StringTokenizer(aQuery, delims, true);
    boolean inTag = false;
    while (tokenizer.hasMoreTokens()) {
      String tok = tokenizer.nextToken();
      if ("<".equals(tok)) {
        inTag = true;
      } else if (">".equals(tok) && inTag) {
        inTag = false;
      } else if (delims.indexOf(tok) == -1) // token is not a delimiter
      {
        if (inTag) {
          if (!tok.startsWith("/")) // ignore end tags
          {
            if (tok.endsWith("/")) {
              tok = tok.substring(0, tok.length() - 1); // strip trailing / from empty tags
            }
            typeList.add(aTypeNamespace + '.' + tok);
          }
        } else {
          keywordList.add(tok);
        }
      }
    }

    // System.out.println(typeList);
    // System.out.println(keywordList);

    setInitiallySelectedTypes((String[]) typeList.toArray(new String[0]));
    display();
    applyBoldfaceToKeywords((String[]) keywordList.toArray(new String[0]));
  }

  /**
   * Configures the viewer appropriately for displaying a hit against an XML fragments query. This
   * does not use a sophisticated algorithm for determining the location of the document that
   * matched the query. Currently all it does is call {@link #setInitiallySelectedTypes(String[])}
   * with the list of types mentioned in the query and {@link #applyBoldfaceToKeywords(String[])} on
   * any keywords mentioned in the query.
   * 
   * @param aQuery
   *          an XML fragments query
   */
  public void configureViewForXmlFragmentsQuery(String aQuery) {
    configureViewForXmlFragmentsQuery(aQuery, "*");
  }

  /**
   * Assign initially checked to the specified types, pairing up down the lists
   * 
   * @param aNotChecked
   *          list of types not to be initially checked JMP
   */
  public void assignCheckedFromList(ArrayList aNotChecked) {
    Iterator iterC = aNotChecked.iterator();
    while (iterC.hasNext()) {
      String typeName = (String) iterC.next();
      // assign to list of types not to be initially checked
      noCheckSet.add(typeName);
    }
  }

  /**
   * Assign colors to the specified types, pairing up down the lists
   * 
   * @param aColors
   *          list of colors
   * @param aTypeNames
   *          list of type names JMP
   */
  public void assignColorsFromList(List aColors, ArrayList aTypeNames) {
    // populate mTypeNameToColorMap
    Iterator iter = aTypeNames.iterator();
    Iterator iterC = aColors.iterator();
    while (iter.hasNext()) {
      if (!iterC.hasNext())
        break;
      String typeName = (String) iter.next();
      Color color = (Color) iterC.next();
      // assign background color
      mTypeNameToColorMap.put(typeName, color);
    }

    setUserTypes(aTypeNames);

    // clear checkbox panel so it will be refreshed
    annotationCheckboxPanel.removeAll();
    mTypeToCheckboxMap.clear();
  }

  /**
   * Assign colors to the specified types
   * 
   * @param aTypeNames
   *          list of type names
   */
  private void assignColors(List aTypeNames) {
    // populate mTypeNameToColorMap
    Iterator iter = aTypeNames.iterator();
    while (iter.hasNext()) {
      String typeName = (String) iter.next();
      // assign background color
      Color c = COLORS[mTypeNameToColorMap.size() % COLORS.length];
      mTypeNameToColorMap.put(typeName, c);
    }

    // clear checkbox panel so it will be refreshed
    annotationCheckboxPanel.removeAll();
    mTypeToCheckboxMap.clear();
  }

  /**
   * Creates/updates the display. This is called when setCAS() is called and again each time to
   * user's mode or checkbox selections change.
   */
  private void display() {
    // remember split pane divider location so we can restore it later
    int dividerLoc = vertSplitPane.getDividerLocation();

    // remember caret pos and scroll position
    int caretPos = this.textPane.getCaretPosition();
    int verticalScrollPos = this.textScrollPane.getVerticalScrollBar().getValue();

    // type of display depends on whether we are in annotation or entity mode
    switch (mViewMode) {
      case MODE_ANNOTATIONS:
        displayAnnotations();
        break;
      case MODE_ENTITIES:
        displayEntities();
        break;
    }

    // apply boldface to keywords and spans as indicated by user
    doBoldface();

    // update the label of the Show/Hide Unselected Button
    if (mHideUnselectedCheckboxes) {
      showHideUnselectedButton.setText("Show Unselected");
    } else {
      showHideUnselectedButton.setText("Hide Unselected");
    }

    // reset scroll position
    textPane.setCaretPosition(caretPos);
    textScrollPane.getVerticalScrollBar().setValue(verticalScrollPos);
    textScrollPane.revalidate();

    // reset split pane divider
    vertSplitPane.setDividerLocation(dividerLoc);
  }

  /**
   * Creates the annotation display.
   */
  private void displayAnnotations() {
    // for speed, detach document from text pane before updating
    StyledDocument doc = (StyledDocument) textPane.getDocument();
    Document blank = new DefaultStyledDocument();
    textPane.setDocument(blank);

    // make sure annotationCheckboxPanel is showing
    if (legendScrollPane.getViewport().getView() != annotationCheckboxPanel) {
      legendScrollPane.setViewportView(annotationCheckboxPanel);
    }

    // add text from CAS
    try {
      doc.remove(0, doc.getLength());
      doc.insertString(0, mCAS.getDocumentText(), new SimpleAttributeSet());
    } catch (BadLocationException e) {
      throw new RuntimeException(e);
    }

    // Iterate over annotations
    FSIterator iter = mCAS.getAnnotationIndex().iterator();
    Hashtable checkBoxes = new Hashtable();
    HashSet checkBoxesDone = new HashSet();
    while (iter.isValid()) {
      AnnotationFS fs = (AnnotationFS) iter.get();
      iter.moveToNext();

      Type type = fs.getType();

      // have we seen this type before?
      JCheckBox checkbox = (JCheckBox) mTypeToCheckboxMap.get(type);
      if (checkbox == null) {
        // check that type should be displayed
        if ((mDisplayedTypeNames == null || typeNamesContains(mDisplayedTypeNames, type.getName()))
                && !typeNamesContains(mHiddenTypeNames, type.getName())) {
          // if mTypeNameToColorMap exists, get color from there
          Color c = (Color) mTypeNameToColorMap.get(type.getName());
          if (c == null) // assign next available color
          {
            c = COLORS[mTypeNameToColorMap.size() % COLORS.length];
            mTypeNameToColorMap.put(type.getName(), c);
          }
          // This next section required until priorities work properly
          // HashSet noCheckSet = new HashSet();
          String noCheckArray[] = {
          // "org.apache.jresporator.PROPER",
          // "DOCSTRUCT_ANNOT_TYPE",
          // "VOCAB_ANNOT_TYPE"
          };
          for (int i = 0; i < noCheckArray.length; i++) {
            noCheckSet.add(noCheckArray[i]);
          }
          // end of section

          // should type be initially selected?
          boolean selected = ((mInitiallySelectedTypeNames == null &&
          // document annotation is not initially selected in default case
                  !CAS.TYPE_NAME_DOCUMENT_ANNOTATION.equals(type.getName()) && !noCheckSet
                  .contains(type.getName()) // priorities JMP
          ) || (mInitiallySelectedTypeNames != null && typeNamesContains(
                  mInitiallySelectedTypeNames, type.getName())));

          // add checkbox
          checkbox = new JCheckBox(type.getShortName(), selected);
          checkbox.setToolTipText(type.getName());
          checkbox.addActionListener(this);
          checkbox.setBackground(c);
          // annotationCheckboxPanel.add(checkbox); do it later JMP
          checkBoxes.put(type.getName(), checkbox);
          checkBoxesDone.add(checkbox);
          // add to (Type, Checkbox) map
          mTypeToCheckboxMap.put(type, checkbox);
        } else {
          // this type is not hidden, skip it
          continue;
        }
      }
      // if checkbox is checked, assign color to text
      if (checkbox.isSelected()) {
        int begin = fs.getBegin();
        int end = fs.getEnd();

        // Be careful of 0-length annotations and annotations that span the
        // entire document. In either of these cases, if we try to set
        // background color, it will set the input text style, which is not
        // what we want.
        if (begin == 0 && end == mCAS.getDocumentText().length()) {
          end--;
        }

        if (begin < end) {
          MutableAttributeSet attrs = new SimpleAttributeSet();
          StyleConstants.setBackground(attrs, checkbox.getBackground());
          doc.setCharacterAttributes(begin, end - begin, attrs, false);
        }
      }
    }

    // now populate panel with checkboxes in order specified in user file. JMP
    ArrayList aTypeNames = getUserTypes();
    if (aTypeNames != null) {
      Iterator iterT = aTypeNames.iterator();
      while (iterT.hasNext()) {
        String typeName = (String) iterT.next();
        JCheckBox cb = (JCheckBox) checkBoxes.get(typeName);
        if (cb != null) {
          annotationCheckboxPanel.add(cb);
          checkBoxesDone.remove(cb);
        }
      }
    }
    // add additional checkboxes in alphabetical order
    LinkedList checkboxes = new LinkedList(checkBoxesDone);
    Collections.sort(checkboxes, new Comparator() {
      public int compare(Object o1, Object o2) {
        return ((JCheckBox) o1).getText().toLowerCase().compareTo(
                ((JCheckBox) o2).getText().toLowerCase());
      }
    });
    Iterator iterC = checkboxes.iterator();
    while (iterC.hasNext()) {
      JCheckBox cb = (JCheckBox) iterC.next();
      annotationCheckboxPanel.add(cb);
    }

    // add/remove checkboxes from display as determined by the
    // mHideUnselectedCheckboxes toggle
    Iterator cbIter = mTypeToCheckboxMap.values().iterator();
    while (cbIter.hasNext()) {
      JCheckBox cb = (JCheckBox) cbIter.next();
      if (mHideUnselectedCheckboxes && !cb.isSelected()) {
        if (cb.getParent() == annotationCheckboxPanel) {
          annotationCheckboxPanel.remove(cb);
        }
      } else if (cb.getParent() != annotationCheckboxPanel) {
        annotationCheckboxPanel.add(cb);
      }
    }

    // reattach document to text pane
    textPane.setDocument(doc);
  }

  /**
   * Creates the entity display.
   */
  private void displayEntities() {
    // for speed, detach document from text pane before updating
    StyledDocument doc = (StyledDocument) textPane.getDocument();
    Document blank = new DefaultStyledDocument();
    textPane.setDocument(blank);

    // make sure entityCheckboxPanel is showing
    if (legendScrollPane.getViewport().getView() != entityCheckboxPanel) {
      legendScrollPane.setViewportView(entityCheckboxPanel);
    }

    // add text from CAS
    try {
      doc.remove(0, doc.getLength());
      doc.insertString(0, mCAS.getDocumentText(), new SimpleAttributeSet());
    } catch (BadLocationException e) {
      throw new RuntimeException(e);
    }

    // Iterate over EntityAnnotations using JCAS, because the EntityResolver interface
    // uses JCAS as a convenience to the user.
    JCas jcas;
    try {
      // NOTE: for a large type system, this can take a few seconds, which results in a
      // noticeable delay when the user first switches to Entity mode.
      jcas = mCAS.getJCas();
    } catch (CASException e) {
      throw new RuntimeException(e);
    }
    FSIterator iter = jcas.getAnnotationIndex().iterator();
    while (iter.isValid()) {
      Annotation annot = (Annotation) iter.get();
      iter.moveToNext();

      // find out what entity this annotation represents
      EntityResolver.Entity entity = mEntityResolver.getEntity(annot);

      //if not an entity, skip it
      if (entity == null)
        continue;
      
      // have we seen this entity before?
      JCheckBox checkbox = (JCheckBox) mEntityToCheckboxMap.get(entity);
      if (checkbox == null) {
        // assign next available color
        Color c = COLORS[mEntityToCheckboxMap.size() % COLORS.length];
        // add checkbox
        checkbox = new JCheckBox(entity.getCanonicalForm(), true);
        checkbox.setToolTipText(entity.getCanonicalForm());
        checkbox.addActionListener(this);
        checkbox.setBackground(c);
        entityCheckboxPanel.add(checkbox);
        // add to (Entity, Checkbox) map
        mEntityToCheckboxMap.put(entity, checkbox);
      }

      // if checkbox is checked, assign color to text
      if (checkbox.isSelected()) {
        int begin = annot.getBegin();
        int end = annot.getEnd();
        // be careful of 0-length annotation. If we try to set background color when there
        // is no selection, it will set the input text style, which is not what we want.
        if (begin != end) {
          MutableAttributeSet attrs = new SimpleAttributeSet();
          StyleConstants.setBackground(attrs, checkbox.getBackground());
          doc.setCharacterAttributes(begin, end - begin, attrs, false);
        }
      }
    }

    // add/remove checkboxes from display as determined by the
    // mHideUnselectedCheckboxes toggle
    Iterator cbIter = mEntityToCheckboxMap.values().iterator();
    while (cbIter.hasNext()) {
      JCheckBox cb = (JCheckBox) cbIter.next();
      if (mHideUnselectedCheckboxes && !cb.isSelected()) {
        if (cb.getParent() == entityCheckboxPanel) {
          entityCheckboxPanel.remove(cb);
        }
      } else if (cb.getParent() != entityCheckboxPanel) {
        entityCheckboxPanel.add(cb);
      }
    }

    // reattach document to text pane
    textPane.setDocument(doc);
  }

  /**
   * Refreshes the selected annotation tree.
   * 
   * @param aPosition
   *          the currently selected offset into the document. All annotations overlapping this
   *          point will be rendered in the tree.
   */
  private void updateSelectedAnnotationTree(int aPosition) {
    DefaultMutableTreeNode root = (DefaultMutableTreeNode) this.selectedAnnotationTreeModel
            .getRoot();
    root.removeAllChildren();
    FSIterator annotIter = this.mCAS.getAnnotationIndex().iterator();
    while (annotIter.isValid()) {
      AnnotationFS annot = (AnnotationFS) annotIter.get();
      // if (getPanePosition(annot.getBegin()) <= aPosition
      // && getPanePosition(annot.getEnd()) > aPosition)
      if (annot.getBegin() <= aPosition && annot.getEnd() > aPosition) {
        JCheckBox checkbox = (JCheckBox) mTypeToCheckboxMap.get(annot.getType());
        if (checkbox != null && checkbox.isSelected()) {
          addAnnotationToTree(annot);
        }
      }
      // else if (getPanePosition(annot.getBegin()) > aPosition)
      else if (annot.getBegin() > aPosition)
        break;
      annotIter.moveToNext();
    }
    this.selectedAnnotationTreeModel.nodeStructureChanged(root);
    // expand first level
    // int row = 0;
    // while (row < this.selectedAnnotationTree.getRowCount())
    // {
    // if (this.selectedAnnotationTree.getPathForRow(row).getPathCount() <= 2)
    // {
    // this.selectedAnnotationTree.expandRow(row);
    // }
    // row++;
    // }

    // hmmm.. how to get scroll pane to resize properly??
    this.selectedAnnotationTree.treeDidChange();
    // this.selectedAnnotationTree.setPreferredSize(this.selectedAnnotationTree.getSize());
    this.selectedAnnotationTree.revalidate();
    this.horizSplitPane.revalidate();
  }

  /**
   * Adds an annotation to the selected annotations tree. Annotations in the tree are grouped by
   * type.
   * 
   * @param aAnnotation
   *          the annotation to add
   */
  protected void addAnnotationToTree(AnnotationFS aAnnotation) {
    DefaultMutableTreeNode root = (DefaultMutableTreeNode) this.selectedAnnotationTreeModel
            .getRoot();
    // try to find a node for the type
    DefaultMutableTreeNode typeNode = null;
    Enumeration typeNodes = root.children();
    while (typeNodes.hasMoreElements()) {
      DefaultMutableTreeNode node = (DefaultMutableTreeNode) typeNodes.nextElement();
      if (aAnnotation.getType().equals(((TypeTreeNodeObject) node.getUserObject()).getType())) {
        typeNode = node;
        break;
      }
    }
    if (typeNode == null) {
      typeNode = new DefaultMutableTreeNode(new TypeTreeNodeObject(aAnnotation.getType()));
      root.insert(typeNode, 0);
    }

    // add annotation node
    DefaultMutableTreeNode annotationNode = new DefaultMutableTreeNode(new FsTreeNodeObject(
            aAnnotation, null));
    typeNode.insert(annotationNode, 0);
    // add child nodes for features
    addFeatureTreeNodes(annotationNode, aAnnotation);
  }

  private void addFeatureTreeNodes(DefaultMutableTreeNode aParentNode, FeatureStructure aFS) {
    List aFeatures = aFS.getType().getFeatures();
    Iterator iter = aFeatures.iterator();
    while (iter.hasNext()) {
      Feature feat = (Feature) iter.next();
      String featName = feat.getShortName();
      // skip hidden features
      if (mHiddenFeatureNames.contains(featName)) {
        continue;
      }
      // how we get feature value depends on feature's range type)
      String featVal = "null";
      Type rangeType = feat.getRange();
      String rangeTypeName = rangeType.getName();
      if (mCAS.getTypeSystem().subsumes(mStringType, rangeType)) {
        featVal = aFS.getStringValue(feat);
        if (featVal == null) {
          featVal = "null";
        } else if (featVal.length() > 64) {
          featVal = featVal.substring(0, 64) + "...";
        }
      } else if (rangeType.isPrimitive()) {
        featVal = aFS.getFeatureValueAsString(feat);
      } else if (mCAS.getTypeSystem().subsumes(mFsArrayType, rangeType)) {
        ArrayFS arrayFS = (ArrayFS) aFS.getFeatureValue(feat);
        if (arrayFS != null) {
          // Add featName = FSArray node, then add each array element as a child
          DefaultMutableTreeNode arrayNode = new DefaultMutableTreeNode(featName + " = FSArray");
          for (int i = 0; i < arrayFS.size(); i++) {
            FeatureStructure fsVal = arrayFS.get(i);
            if (fsVal != null) {
              // Add the FS node and a dummy child, so that user can expand it.
              // When user expands it, new nodes for feature values will be created.
              DefaultMutableTreeNode fsValNode = new DefaultMutableTreeNode(new FsTreeNodeObject(
                      fsVal, featName));
              if (!fsVal.getType().getFeatures().isEmpty()) {
                fsValNode.add(new DefaultMutableTreeNode(null));
              }
              arrayNode.add(fsValNode);
            } else {
              arrayNode.add(new DefaultMutableTreeNode("null"));
            }
          }
          aParentNode.add(arrayNode);
          continue;
        }
      } else if (rangeType.isArray()) // primitive array
      {
        String[] vals = null;
        if (CAS.TYPE_NAME_STRING_ARRAY.equals(rangeTypeName)) {
          StringArrayFSImpl arrayFS = (StringArrayFSImpl) aFS.getFeatureValue(feat);
          if (arrayFS != null)
            vals = arrayFS.toArray();
        } else if (CAS.TYPE_NAME_INTEGER_ARRAY.equals(rangeTypeName)) {
          IntArrayFSImpl arrayFS = (IntArrayFSImpl) aFS.getFeatureValue(feat);
          if (arrayFS != null)
            vals = arrayFS.toStringArray();
        } else if (CAS.TYPE_NAME_FLOAT_ARRAY.equals(rangeTypeName)) {
          FloatArrayFSImpl arrayFS = (FloatArrayFSImpl) aFS.getFeatureValue(feat);
          if (arrayFS != null)
            vals = arrayFS.toStringArray();
        } else if (CAS.TYPE_NAME_BOOLEAN_ARRAY.equals(rangeTypeName)) {
          BooleanArrayFSImpl arrayFS = (BooleanArrayFSImpl) aFS.getFeatureValue(feat);
          if (arrayFS != null)
            vals = arrayFS.toStringArray();
        } else if (CAS.TYPE_NAME_BYTE_ARRAY.equals(rangeTypeName)) {
          ByteArrayFSImpl arrayFS = (ByteArrayFSImpl) aFS.getFeatureValue(feat);
          if (arrayFS != null)
            vals = arrayFS.toStringArray();
        } else if (CAS.TYPE_NAME_SHORT_ARRAY.equals(rangeTypeName)) {
          ShortArrayFSImpl arrayFS = (ShortArrayFSImpl) aFS.getFeatureValue(feat);
          if (arrayFS != null)
            vals = arrayFS.toStringArray();
        } else if (CAS.TYPE_NAME_LONG_ARRAY.equals(rangeTypeName)) {
          LongArrayFSImpl arrayFS = (LongArrayFSImpl) aFS.getFeatureValue(feat);
          if (arrayFS != null)
            vals = arrayFS.toStringArray();
        }
        if (CAS.TYPE_NAME_DOUBLE_ARRAY.equals(rangeTypeName)) {
          DoubleArrayFSImpl arrayFS = (DoubleArrayFSImpl) aFS.getFeatureValue(feat);
          if (arrayFS != null)
            vals = arrayFS.toStringArray();
        }
        if (vals == null) {
          featVal = "null";
        } else {
          StringBuffer displayVal = new StringBuffer();
          displayVal.append('[');
          for (int i = 0; i < vals.length - 1; i++) {
            displayVal.append(vals[i]);
            displayVal.append(',');
          }
          if (vals.length > 0) {
            displayVal.append(vals[vals.length - 1]);
          }
          displayVal.append(']');
          featVal = displayVal.toString();
        }
      } else
      // single feature value
      {
        FeatureStructure fsVal = aFS.getFeatureValue(feat);
        if (fsVal != null) {
          // Add the FS node and a dummy child, so that user can expand it.
          // When user expands it, new nodes for feature values will be created.
          DefaultMutableTreeNode fsValNode = new DefaultMutableTreeNode(new FsTreeNodeObject(fsVal,
                  featName));
          if (!fsVal.getType().getFeatures().isEmpty()) {
            fsValNode.add(new DefaultMutableTreeNode(null));
          }
          aParentNode.add(fsValNode);
          continue;
        }
      }
      aParentNode.add(new DefaultMutableTreeNode(featName + " = " + featVal));
    }
  }

  /**
   * Does wildcard matching to determine if a give type name is "contained" in a Set of type names.
   * 
   * @param names
   *          Type names, which may include wildcards (e.g, uima.tt.*)
   * @param name
   *          A type name
   * @return True iff name matches a name in type names
   */
  private boolean typeNamesContains(Set names, String name) {
    if (names.contains(name))
      return true;
    else {
      Iterator namesIterator = names.iterator();
      while (namesIterator.hasNext()) {
        String otherName = (String) namesIterator.next();
        if (otherName.indexOf('*') != -1) {
          if (wildCardMatch(name, otherName)) {
            return true;
          }
        } else {
          if (otherName.equalsIgnoreCase(name)) {
            return true;
          }
        }
      }
    }
    return false;
  }

  /**
   * Helper for {@link #typeNamesContains(HashSet, String)}.
   * 
   * @param s
   *          A litteral string
   * @param pattern
   *          A string that includes one or more *'s as wildcards
   * @return True iff the string matches the pattern.
   */
  private boolean wildCardMatch(String s, String pattern) {
    StringBuffer regexpPatternBuffer = new StringBuffer();
    for (int i = 0; i < pattern.length(); i++) {
      char c = pattern.charAt(i);
      if (c == '*')
        regexpPatternBuffer.append('.');
      else if (c == '.')
        regexpPatternBuffer.append('\\');
      if (Character.isLetter(c)) {
        regexpPatternBuffer.append('(').append(Character.toLowerCase(c)).append('|').append(
                Character.toUpperCase(c)).append(')');
      } else {
        regexpPatternBuffer.append(c);
      }
    }

    return s.matches(new String(regexpPatternBuffer));
  }

  /**
   * @see java.awt.Component#setSize(Dimension)
   */
  public void setSize(Dimension d) {
    super.setSize(d);
    Insets insets = getInsets();
    Dimension paneSize = new Dimension(d.width - insets.left - insets.right, d.height - insets.top
            - insets.bottom);

    horizSplitPane.setPreferredSize(paneSize);
    horizSplitPane.setSize(paneSize);
  }

  /*
   * (non-Javadoc)
   * 
   * @see java.awt.event.ActionListener#actionPerformed(java.awt.event.ActionEvent)
   */
  public void actionPerformed(ActionEvent e) {
    if (e.getSource() == selectAllButton) {
      Iterator cbIter = (mViewMode == MODE_ANNOTATIONS) ? mTypeToCheckboxMap.values().iterator()
              : mEntityToCheckboxMap.values().iterator();
      while (cbIter.hasNext()) {
        ((JCheckBox) cbIter.next()).setSelected(true);
      }
      display();
    } else if (e.getSource() == deselectAllButton) {
      Iterator cbIter = (mViewMode == MODE_ANNOTATIONS) ? mTypeToCheckboxMap.values().iterator()
              : mEntityToCheckboxMap.values().iterator();
      while (cbIter.hasNext()) {
        ((JCheckBox) cbIter.next()).setSelected(false);
      }
      display();
    } else if (e.getSource() == annotationModeButton) {
      mViewMode = MODE_ANNOTATIONS;
      display();
    } else if (e.getSource() == entityModeButton) {
      mViewMode = MODE_ENTITIES;
      display();
      // make sure we clear the annotation tree when we go into entity mode
      // this.updateSelectedAnnotationTree(0);
    } else if (e.getSource() == showHideUnselectedButton) {
      mHideUnselectedCheckboxes = !mHideUnselectedCheckboxes;
      display();
    } else if (e.getSource() instanceof JCheckBox) {
      display();
    }
  }

  /*
   * (non-Javadoc)
   * 
   * @see java.awt.event.MouseListener#mouseClicked(java.awt.event.MouseEvent)
   */
  public void mouseClicked(MouseEvent e) {
    if (mViewMode == MODE_ANNOTATIONS) {
      int pos = textPane.viewToModel(e.getPoint());
      this.updateSelectedAnnotationTree(pos);
    }
  }

  /*
   * (non-Javadoc)
   * 
   * @see java.awt.event.MouseListener#mouseEntered(java.awt.event.MouseEvent)
   */
  public void mouseEntered(MouseEvent e) {
  }

  /*
   * (non-Javadoc)
   * 
   * @see java.awt.event.MouseListener#mouseExited(java.awt.event.MouseEvent)
   */
  public void mouseExited(MouseEvent e) {
  }

  /*
   * (non-Javadoc)
   * 
   * @see java.awt.event.MouseListener#mousePressed(java.awt.event.MouseEvent)
   */
  public void mousePressed(MouseEvent e) {
  }

  /*
   * (non-Javadoc)
   * 
   * @see java.awt.event.MouseListener#mouseReleased(java.awt.event.MouseEvent)
   */
  public void mouseReleased(MouseEvent e) {
  }

  /**
   * Inner class containing data for a tree node representing a FeatureStructure
   */
  static class FsTreeNodeObject {
    public FsTreeNodeObject(FeatureStructure aFS, String aFeatureName) {
      mFS = aFS;
      mFeatureName = aFeatureName;
      mCaption = mFS.getType().getShortName();
      if (mFS instanceof AnnotationFS) {
        String coveredText = ((AnnotationFS) mFS).getCoveredText();
        if (coveredText.length() > 64)
          coveredText = coveredText.substring(0, 64) + "...";
        mCaption += " (\"" + coveredText + "\")";
      }
      if (mFeatureName != null) {
        mCaption = mFeatureName + " = " + mCaption;
      }
    }

    public FeatureStructure getFS() {
      return mFS;
    }

    public String toString() {
      return mCaption;
    }

    private FeatureStructure mFS;

    private String mFeatureName;

    private String mCaption;
  }

  /**
   * Inner class containing data for a tree node representing a Type
   */
  static class TypeTreeNodeObject {
    public TypeTreeNodeObject(Type aType) {
      mType = aType;
    }

    public Type getType() {
      return mType;
    }

    public String toString() {
      return mType.getShortName();
    }

    private Type mType;
  }

  class AnnotationTreeCellRenderer extends DefaultTreeCellRenderer {
    private static final long serialVersionUID = -8661556785397184756L;

    /*
     * (non-Javadoc)
     * 
     * @see javax.swing.tree.TreeCellRenderer#getTreeCellRendererComponent(javax.swing.JTree,
     *      java.lang.Object, boolean, boolean, boolean, int, boolean)
     */
    public Component getTreeCellRendererComponent(JTree tree, Object value, boolean sel,
            boolean expanded, boolean leaf, int row, boolean aHasFocus) {

      // set background color if this is an Annotation or a Type
      Color background = null;
      if (value instanceof DefaultMutableTreeNode) {
        Object userObj = ((DefaultMutableTreeNode) value).getUserObject();
        Type type = null;
        if (userObj instanceof FsTreeNodeObject) {
          FeatureStructure fs = ((FsTreeNodeObject) userObj).getFS();
          type = fs.getType();
        } else if (userObj instanceof TypeTreeNodeObject) {
          type = ((TypeTreeNodeObject) userObj).getType();
        }
        if (type != null) {
          // look up checkbox to get color
          JCheckBox checkbox = (JCheckBox) mTypeToCheckboxMap.get(type);
          if (checkbox != null) {
            background = checkbox.getBackground();
          }
        }
      }
      this.setBackgroundNonSelectionColor(background);
      this.setBackgroundSelectionColor(background);

      Component component = super.getTreeCellRendererComponent(tree, value, sel, expanded, leaf,
              row, aHasFocus);
      return component;
    }
  }

  /*
   * (non-Javadoc)
   * 
   * @see javax.swing.event.TreeWillExpandListener#treeWillCollapse(javax.swing.event.TreeExpansionEvent)
   */
  public void treeWillCollapse(TreeExpansionEvent event) throws ExpandVetoException {
  }

  /*
   * (non-Javadoc)
   * 
   * @see javax.swing.event.TreeWillExpandListener#treeWillExpand(javax.swing.event.TreeExpansionEvent)
   */
  public void treeWillExpand(TreeExpansionEvent event) throws ExpandVetoException {
    // if FS node is expanded and it has a dummy child, replace with
    // feature value nodes (this is what lets us do infinite tree)
    Object lastPathComponent = event.getPath().getLastPathComponent();
    if (lastPathComponent instanceof DefaultMutableTreeNode) {
      DefaultMutableTreeNode expandedNode = (DefaultMutableTreeNode) lastPathComponent;
      Object userObj = expandedNode.getUserObject();
      if (userObj instanceof FsTreeNodeObject) {
        TreeNode firstChild = expandedNode.getFirstChild();
        if (firstChild instanceof DefaultMutableTreeNode
                && ((DefaultMutableTreeNode) firstChild).getUserObject() == null) {
          expandedNode.removeAllChildren();
          FeatureStructure fs = ((FsTreeNodeObject) userObj).getFS();
          addFeatureTreeNodes(expandedNode, fs);
          ((JTree) event.getSource()).treeDidChange();
        }

      }
    }
  }

  /*
   * (non-Javadoc)
   * 
   * @see javax.swing.event.TreeExpansionListener#treeCollapsed(javax.swing.event.TreeExpansionEvent)
   */
  public void treeCollapsed(TreeExpansionEvent event) {
  }

  /*
   * (non-Javadoc)
   * 
   * @see javax.swing.event.TreeExpansionListener#treeExpanded(javax.swing.event.TreeExpansionEvent)
   */
  public void treeExpanded(TreeExpansionEvent event) {
    // if a Type node is expanded and has only one child,
    // also expand this child (a usability improvement)
    Object lastPathComponent = event.getPath().getLastPathComponent();
    if (lastPathComponent instanceof DefaultMutableTreeNode) {
      DefaultMutableTreeNode expandedNode = (DefaultMutableTreeNode) lastPathComponent;
      Object userObj = expandedNode.getUserObject();
      if (userObj instanceof TypeTreeNodeObject && expandedNode.getChildCount() == 1) {
        TreePath childPath = event.getPath().pathByAddingChild(expandedNode.getFirstChild());
        ((JTree) event.getSource()).expandPath(childPath);
        ((JTree) event.getSource()).treeDidChange();
      }
    }
  }

  /*
   * (non-Javadoc)
   * 
   * @see java.awt.event.ItemListener#itemStateChanged(java.awt.event.ItemEvent)
   */
  public void itemStateChanged(ItemEvent e) {
    if (e.getSource() == sofaSelectionComboBox) {
      // a new sofa was selected. Switch to that view and update display
      String sofaId = (String) e.getItem();
      if ("DEFAULT".equals(sofaId)) {
        mCAS = mCAS.getView(CAS.NAME_DEFAULT_SOFA);
      } else {
        mCAS = mCAS.getView(sofaId);
      }
      display();
    }

  }

  /**
   * Applies boldface as per the mBoldfaceKeywords and mBoldfaceSpans fields.
   */
  private void doBoldface() {
    // Keywords
    if (mBoldfaceKeywords.length > 0) {
      // build regular expression
      StringBuffer regEx = new StringBuffer();
      for (int i = 0; i < mBoldfaceKeywords.length; i++) {
        if (i > 0) {
          regEx.append('|');
        }
        regEx.append("\\b");
        String word = mBoldfaceKeywords[i];
        for (int j = 0; j < word.length(); j++) {
          char c = word.charAt(j);
          if (Character.isLetter(c)) {
            regEx.append('[').append(Character.toLowerCase(c)).append(Character.toUpperCase(c))
                    .append(']');
          } else if (c == '.' || c == '^' || c == '&' || c == '\\' || c == '(' || c == ')') {
            regEx.append('\\').append(c);
          } else {
            regEx.append('c');
          }
        }
        regEx.append("\\b");
      }
      // System.out.println("RegEx: " + regEx);
      Pattern pattern = Pattern.compile(regEx.toString());
      Matcher matcher = pattern.matcher(mCAS.getDocumentText());
      // match
      int pos = 0;
      while (matcher.find(pos)) {
        MutableAttributeSet attrs = new SimpleAttributeSet();
        StyleConstants.setBold(attrs, true);
        StyledDocument doc = (StyledDocument) textPane.getDocument();
        doc.setCharacterAttributes(matcher.start(), matcher.end() - matcher.start(), attrs, false);

        if (pos == matcher.end()) // infinite loop check
          break;
        else
          pos = matcher.end();
      }
    }
    // Spans
    int docLength = mCAS.getDocumentText().length();
    int len = mBoldfaceSpans.length;
    len -= len % 2; // to avoid ArrayIndexOutOfBoundsException if some numbskull passes in an
    // odd-length array
    int i = 0;
    while (i < len) {
      int begin = mBoldfaceSpans[i];
      int end = mBoldfaceSpans[i + 1];
      if (begin >= 0 && begin <= docLength && end >= 0 && end <= docLength) {
        MutableAttributeSet attrs = new SimpleAttributeSet();
        StyleConstants.setBold(attrs, true);
        StyledDocument doc = (StyledDocument) textPane.getDocument();
        doc.setCharacterAttributes(begin, end - begin, attrs, false);
      }
      i += 2;
    }
  }

  /**
   * Gets the selected annotation tree component.
   * 
   * @return the tree that displays annotation details about annotations overlapping the point where
   *         the user last clicked in the text.
   */
  protected JTree getSelectedAnnotationTree() {
    return this.selectedAnnotationTree;
  }

  /**
   * A panel that is to be placed in a JScrollPane that can only scroll vertically. This panel
   * should have its width track the viewport's width, and increase its height as necessary to
   * display all components.
   * 
   * 
   */
  static class VerticallyScrollablePanel extends JPanel implements Scrollable {
    private static final long serialVersionUID = 1009744410018634511L;

    /*
     * (non-Javadoc)
     * 
     * @see javax.swing.Scrollable#getPreferredScrollableViewportSize()
     */
    public Dimension getPreferredScrollableViewportSize() {
      return getPreferredSize();
    }

    /*
     * (non-Javadoc)
     * 
     * @see javax.swing.Scrollable#getScrollableBlockIncrement(java.awt.Rectangle, int, int)
     */
    public int getScrollableBlockIncrement(Rectangle visibleRect, int orientation, int direction) {
      return 50;
    }

    /*
     * (non-Javadoc)
     * 
     * @see javax.swing.Scrollable#getScrollableTracksViewportHeight()
     */
    public boolean getScrollableTracksViewportHeight() {
      return false;
    }

    /*
     * (non-Javadoc)
     * 
     * @see javax.swing.Scrollable#getScrollableTracksViewportWidth()
     */
    public boolean getScrollableTracksViewportWidth() {
      return true;
    }

    /*
     * (non-Javadoc)
     * 
     * @see javax.swing.Scrollable#getScrollableUnitIncrement(java.awt.Rectangle, int, int)
     */
    public int getScrollableUnitIncrement(Rectangle visibleRect, int orientation, int direction) {
      return 10;
    }

  }
  
  /**
   * Trivial entity resolver that's applied if the user turns on entity mode without
   * specifying their own entity resolver.  Returns the covered text as the canonical form,
   * and treats annotations with equal covered text as belonging to the same entity.
   */
  static class DefaultEntityResolver implements EntityResolver {

    /* (non-Javadoc)
     * @see org.apache.uima.tools.viewer.EntityResolver#getCanonicalForm(org.apache.uima.jcas.tcas.Annotation)
     */
    public Entity getEntity(final Annotation aAnnotation) {
      return new Entity() {
        
        public String getCanonicalForm() {          
          return aAnnotation.getCoveredText();
        }

        public boolean equals(Object obj) {
          if (obj instanceof Entity) {
            String canon = ((Entity)obj).getCanonicalForm();
            return getCanonicalForm().equals(canon);
          }
          return false;
        }

        public int hashCode() {          
          return getCanonicalForm().hashCode();
        }              
      };              
    }
    
  }
}
