package jgrasp_viewers;


import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.GridBagConstraints;
import java.awt.Insets;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.event.InputEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseMotionAdapter;
import java.awt.geom.AffineTransform;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.List;
import javax.swing.BorderFactory;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JSplitPane;
import javax.swing.JTextArea;
import javax.swing.SwingUtilities;
import jgrasp.tool.BasicToolUtil;
import jgrasp.tool.URTextArea;
import jgrasp.BNSplitPane;
import jgrasp.Colors;
import jgrasp.Drawing;
import jgrasp.KeyMovement;
import jgrasp.viewer.PaintUtil;
import jgrasp.viewer.Sizes;
import jgrasp.viewer.SubviewerPosition;
import jgrasp.viewer.Util;
import jgrasp.viewer.ViewerCreateData;
import jgrasp.viewer.ViewerException;
import jgrasp.viewer.ViewerInfo;
import jgrasp.viewer.ViewerInitData;
import jgrasp.viewer.ViewerPriorityData;
import jgrasp.viewer.ViewerRoot;
import jgrasp.viewer.ViewerUpdateData;
import jgrasp.viewer.ViewerValueData;
import jgrasp.viewer.gui.SubViewer;
import jgrasp.viewer.gui.ValueDragData;
import jgrasp.viewer.gui.ValueDragger;
import jgrasp.viewer.jgrdi.DebugContext;
import jgrasp.viewer.jgrdi.Method;
import jgrasp.viewer.jgrdi.Value;
import org.w3c.dom.Element;


/** A viewer that shows relative component positions and sizes. **/
public class java__awt__Component_ComponentView extends ViewerRoot {

   /** A component boundary description. **/
   private static class Bounds {
   
      /** The bounds. **/
      private final Rectangle bounds = new Rectangle();
      
      /** The parent bounds, or null if there is no parent. **/
      private Bounds parent;
   
      /** Bounds of all the children. **/
      private Bounds[] children;
   
      /** The component class name. **/
      private String type;
   
      /** The component value. **/
      private Value value;
   
      /** The component value id. **/
      private long id;
   
   
      /** {@inheritDoc} **/
      @Override
      public String toString() {
         StringBuilder sb = new StringBuilder();
         getDescription(sb, "");
         return sb.toString();
      }
   
   
      private void getDescription(final StringBuilder sb, final String indent) {
         sb.append(indent);
         sb.append(getBaseDescription());
         sb.append("\n");
         if (children != null) {
            for (Bounds cb : children) {
               cb.getDescription(sb, indent + "   ");
            }
         }
      }
      
      
      /** Gets a description of the component.
       *
       * @return a one-line description of the component. **/
      private String getBaseDescription() {
         //noinspection StringConcatenationMissingWhitespace
         return type + " (" + bounds.x + ", " + bounds.y + ") " + bounds.width + "x" + bounds.height;
      }
   }

   /** The gui root panel. **/
   private JPanel outerPanel;

   /** The bounds display panel. **/
   private JPanel displayPanel;

   /** Text display (shows types of components under mouse). **/
   private JTextArea textArea;

   /** The boundary hierarchy. This will be accessed by both the debugger and gui threads, so it should be read only
    * once wherever the value must remain consistent. **/
   private Bounds topBounds;
   
   /** The selected bounds. **/
   private Bounds selBounds;
   
   /** The accessible description. **/
   private String accessibleDescription;

   /** True if the mouse is outside the component display, false otherwise. **/
   private boolean mouseOut = true;

   /** The subviewer. **/
   private SubViewer selectionViewer;

   /** The currently selected component, or null if none is selected. **/
   private Value selComponent;

   /** The id of the currently selected component, or -1 if none is selected. **/
   private long selId = -1L;

   /** The new selected value, or null if none was selected. **/
   private Value newSelComponent;

   /** Creation data. **/
   private final ViewerCreateData createData;


   /** Creates a new component viewer.
    *
    * @param vcd viewer creation data. **/
   public java__awt__Component_ComponentView(final ViewerCreateData vcd) {
      super(false, EnumSet.of(CreationFlags.STANDARD_BORDER));
      createData = vcd;
   }


   /** {@inheritDoc} **/
   @Override
   public void build(final ViewerInitData vid, final Element initDataIn) {
      super.build(vid, initDataIn);
      vid.setAutoUpdate(true);
      if (createData.supportsSubViewers()) {
         selectionViewer = SubViewer.createSubViewer(this, vid);
      }
   }


   /** {@inheritDoc} **/
   @Override
   public void buildGui(final JPanel main) {
      buildPanel();
      main.setLayout(new BorderLayout());
      main.add(outerPanel, "Center");
   }


   /** {@inheritDoc} **/
   @Override
   public void destroy() {
      super.destroy();
      selComponent = null;
      newSelComponent = null;
   }


   /** Finds the component at a certain position.
    *
    * @param pos the position to be searched.
    *
    * @param old if this is non-negative and a component with this id is found, select the component found after it,
    * if any. Using this repeatedly will cycle through all the components at a certain position.
    *
    * @param keep if this is non-negative and <code>old</code> is negative, choose the component with this id if it is
    * one of the ones found.
    *
    * @return the bounds of the component found at the specified position, or null if none was found. **/
   private Bounds findComponent(final Point pos, final long old, final long keep) {
      int width = displayPanel.getWidth() - 1;
      int height = displayPanel.getHeight() - 1;
      Bounds bounds = topBounds;
      if (bounds == null) {
         return null;
      }
      Rectangle rect = bounds.bounds;
      double scale = Math.min(width / (double) rect.width, height / (double) rect.height);
      double xOffs = (width - rect.width * scale) / 2.0;
      double yOffs = (height - rect.height * scale) / 2.0;
   
      List<Bounds> result = new ArrayList<>();
      findComponents(pos.x, pos.y, xOffs, yOffs, scale, bounds, 0, result);
      if (result.isEmpty()) {
         return null;
      }
      if (old < 0) {
         if (keep >= 0) {
            for (Bounds bounds1 : result) {
               if (bounds1.id == keep) {
                  return bounds1;
               }
            }
         }
         return result.get(result.size() - 1);
      }
      for (int i = 0; i < result.size(); i++) {
         if (result.get(i).id == old) {
            return i > 0 ? result.get(i - 1) : result.get(result.size() - 1);
         }
      }
      return result.get(result.size() - 1);
   }


   /** {@inheritDoc} **/
   @Override
   public void getInfo(final ViewerInfo vi) {
      vi.setShortDescription("Detail viewer for components");
      vi.setLongDescription("This viewer displays the component shape and the shapes of its descendants. Clicking on "
            + "the viewer repeatedly will cycle the selection through all components under the cursor. Hold down the "
            + "shift key to click and drag out a component without cycling to the next component.");
   }


   /** {@inheritDoc} **/
   @Override
   public Dimension getMainSize() {
      Font font = outerPanel.getFont();
      if (font != null) {
         FontMetrics fm = outerPanel.getFontMetrics(font);
         int ht = fm.getHeight();
         return new Dimension(ht * 15, ht * 7);
      }
      return new Dimension(210, 100);
   }


   /** {@inheritDoc} **/
   @Override
   public int getPriority(final ViewerPriorityData vpd) {
      return 10;
   }


   /** {@inheritDoc} **/
   @Override
   public String getViewName() {
      return "Component";
   }


   /** Responds to mouse motion.
    *
    * @param x mouse x position.
    *
    * @param y mouse y position. **/
   public void handleMouseMove(final int x, final int y) {
      int width = displayPanel.getWidth() - 1;
      int height = displayPanel.getHeight() - 1;
      Bounds bounds = topBounds;
      if (bounds == null) {
         mouseOut = true;
         setTextAreaText("No components contain point " + x + ", " + y);
         return;
      }
   
      Rectangle boundsRect = bounds.bounds;
      double scale = Math.min(width / (double) boundsRect.width, height / (double) boundsRect.height);
      double xOffs = (width - boundsRect.width * scale) / 2.0;
      double yOffs = (height - boundsRect.height * scale) / 2.0;
   
      StringBuilder result = new StringBuilder();
      result.append("Components containing point ");
      result.append(x);
      result.append(", ");
      result.append(y);
      result.append("\n");
      findComponents(x, y, xOffs, yOffs, scale, bounds, 0, result);
      String resultStr = result.toString();
      if (!resultStr.isEmpty()) {
         mouseOut = false;
         setTextAreaText(resultStr);
      }
      else {
         mouseOut = true;
         setTextAreaText(bounds.toString());
      }
   }


   /** Paints the bounds rectangles on the display panel.
    *
    * @param g graphics on which the rendering will be performed. **/
   public void paintDisplay(final Graphics g) {
      int width = displayPanel.getWidth() - 1;
      int height = displayPanel.getHeight() - 1;
      g.setColor(outerPanel.getBackground());
      g.fillRect(0, 0, width + 1, height + 1);
      Bounds bounds = topBounds;
      if (bounds == null) {
         return;
      }
   
      Graphics2D graphics = (Graphics2D) g;
      AffineTransform prevTransform = graphics.getTransform();
      setBaseTransformAndAdjust(g);
      
      Rectangle boundsRect = bounds.bounds;
      double scale = Math.min(width / (double) boundsRect.width, height / (double) boundsRect.height);
      double xOffs = (width - boundsRect.width * scale) / 2.0;
      double yOffs = (height - boundsRect.height * scale) / 2.0;
   
      FontMetrics fm = g.getFontMetrics();
      int fontHeight = fm.getHeight();
      int lw = javaToDrawingW(Sizes.getVeryThinEdge(fontHeight));
   
      Rectangle[] selRect = new Rectangle[1];
      paintBounds((Graphics2D) g, xOffs, yOffs, scale, bounds, 0, lw, selRect);
      if (selRect[0] != null) {
         g.setColor(Drawing.getColor(Colors.RED));
         PaintUtil.drawRect(g, selRect[0], lw);
      }
      
      graphics.setTransform(prevTransform);
   }


   /** Changes the value in the selection viewer based on the current selected value.
    *
    * @param context the current debug context.
    *
    * @param isReset true if the current value should be considered a "new" value, false otherwise. **/
   public void setSelectionViewValue(final DebugContext context, final boolean isReset) {
   
      if (selectionViewer == null) {
         return;
      }
      final Value v;
      v = newSelComponent;
      if (v == null && selComponent == null) {
         return;
      }
      if (v != null && selComponent != null && v.isSameNaN(selComponent)) {
         return;
      }
   
      final boolean prevNullSubviewVal = selComponent == null;
      selComponent = v;
      selId = v == null ? -1L : v.getUniqueID();
      selectionViewer.setValue(v, context, "", " ", isReset);
      SwingUtilities.invokeLater(
         ()-> {
            if ((v == null) != prevNullSubviewVal) {
               getVIData().setSubviewPanel(v == null ? null : selectionViewer.getComponent(),
                     SubviewerPosition.BOTTOM);
            }
         });
         
         
   }


   /** {@inheritDoc} **/
   @Override
   public void updateGui() {
      displayPanel.getAccessibleContext().setAccessibleDescription(accessibleDescription);
   
      Bounds tb = topBounds;
      if (tb == null) {
         return;
      }
      if (selBounds != null || mouseOut) {
         setTextAreaText(accessibleDescription);
      }
      if (displayPanel != null) {
         displayPanel.repaint();
      }
   }


   /** {@inheritDoc} **/
   @Override
   public void updateState(final ViewerValueData valueData, final ViewerUpdateData data, final DebugContext context)
         throws ViewerException {
      Value value = valueData.value();
      Bounds[] sel = new Bounds[1];
      topBounds = getBounds(null, value, context, sel);
      selBounds = sel[0];
      setSelectionViewValue(context, data.isReset());
      
      Bounds bds = (sel[0] == null) ? topBounds : sel[0];
      StringBuilder sb = new StringBuilder();
      sb.append((sel[0] == null) ? "top component" : "component chain from selected component to root");
      while (bds != null) {
         sb.append('\n');
         sb.append(bds.getBaseDescription());
         bds = bds.parent;
      }
      accessibleDescription = sb.toString();
   }


   /** Builds the view panel. **/
   private void buildPanel() {
      outerPanel = 
         new JPanel(new BorderLayout()) {
         
            @Override
            public void updateUI() {
               super.updateUI();
               BorderLayout layout = (BorderLayout) getLayout();
               int gap = 1 + getFont().getSize() / 2;
               layout.setHgap(gap);
               layout.setVgap(gap);
               setBorder(BorderFactory.createEmptyBorder(gap, gap, gap, gap));
            }
            
            
            @Override
            public Color getBackground() {
               return Drawing.getColor(Colors.BG);
            }
            
            
            @Override
            public Color getForeground() {
               return Drawing.getColor(Colors.FG);
            }
         };
      displayPanel = 
         new JPanel() {
         
            @Override
            public Dimension getPreferredSize() {
               return new Dimension(0, 0);
            }
         
         
            @Override
            public void paintComponent(final Graphics g) {
               paintDisplay(g);
            }
            
            
            @Override
            public Color getBackground() {
               return Drawing.getColor(Colors.BG);
            }
            
            
            @Override
            public Color getForeground() {
               return Drawing.getColor(Colors.FG);
            }
         };
      displayPanel.addMouseMotionListener(
         new MouseMotionAdapter() {
         
            @Override
            public void mouseMoved(final MouseEvent e) {
               if (selBounds != null) {
                  return;
               }
               handleMouseMove(e.getX(), e.getY());
            }
         });
      displayPanel.addMouseListener(
         new MouseAdapter() {
         
            @Override
            public void mouseExited(final MouseEvent e) {
               mouseOut = true;
               if (selBounds != null) {
                  return;
               }
               Bounds bounds = topBounds;
               setTextAreaText(bounds == null ? "" : ("top component\n" + bounds));
            }
         
         
            @Override
            public void mousePressed(final MouseEvent e) {
               if ((e.getModifiersEx() & BasicToolUtil.getMenuShortcutKeyMaskEx()) != 0) {
                  setSelComponent(null);
               }
               else {
                  long id = (e.getModifiersEx() & InputEvent.SHIFT_DOWN_MASK) == 0 ? selId : -1;
                  long keep = (e.getModifiersEx() & InputEvent.SHIFT_DOWN_MASK) == 0 ? -1 : selId;
                  setSelComponent(findComponent(e.getPoint(), id, keep));
               }
            }
         });
      ValueDragger.createValueDragger(displayPanel, getVIData(), 
         (pos)-> {
            if (selComponent == null) {
               return null;
            }
            return new ValueDragData(null, selComponent, null, getVIData().getScope(), getClass().getName());
         });
      JSplitPane splitPane = 
         new BNSplitPane(JSplitPane.HORIZONTAL_SPLIT) {
                     
            
            @Override
            public Color getBackground() {
               return Drawing.getColor(Colors.BG);
            }
            
            
            @Override
            public Color getForeground() {
               return Drawing.getColor(Colors.FG);
            }
         };
      splitPane.setResizeWeight(0.5);
      outerPanel.add(splitPane, "Center");
      splitPane.setTopComponent(displayPanel);
      textArea = new URTextArea("Component Hierarchy");
      textArea.setEditable(false);
      splitPane.setBottomComponent(
         new JScrollPane(textArea) {
         
            @Override
            public Dimension getPreferredSize() {
               return new Dimension(0, 0);
            }
         });
   
      GridBagConstraints constraints = new GridBagConstraints();
      Insets insets = constraints.insets;
      int spacing = 4;
      constraints.weightx = 0.001;
      constraints.weighty = 0.001;
      insets.top = spacing;
      insets.bottom = spacing;
      insets.right = spacing;
      insets.left = spacing;
      constraints.gridwidth = GridBagConstraints.REMAINDER;
      constraints.fill = GridBagConstraints.NONE;
      // constraints.anchor = GridBagConstraints.WEST;
   
      Bounds tb = topBounds;
      if (tb != null) {
         setTextAreaText(tb.toString());
      }
   
      displayPanel.getAccessibleContext().setAccessibleName(getViewName() + " viewer for " + getVIData().getName());
      Util.makeFocusable(displayPanel, true);
      Util.setupCopyForAccessibleDescription(displayPanel);
      
      KeyMovement.setup(displayPanel, (dir, block, expand) -> keyMove(dir, block, expand));
   }


   /** Finds the components at a certain position.
    *
    * @param x x position of mouse.
    *
    * @param y y position of mouse.
    *
    * @param xOffset x offset from parent position.
    *
    * @param yOffset y offset from parent position.
    *
    * @param scale scale multiplier from bounds units to graphics units.
    *
    * @param bounds bounds of the component that is being searched.
    *
    * @param level the depth in the hierarchy.
    *
    * @param out list for holding the found components. **/
   private static void findComponents(final int x, final int y, final double xOffset, final double yOffset,
         final double scale, final Bounds bounds, final int level, final List<? super Bounds> out) {
      Rectangle boundsRect = bounds.bounds;
   
      double xOffs = xOffset;
      double yOffs = yOffset;
      if (level > 0) {
         xOffs += boundsRect.x * scale;
         yOffs += boundsRect.y * scale;
      }
   
      Rectangle rect = new Rectangle((int) xOffs, (int) yOffs, (int) (xOffs + boundsRect.width * scale) - (int) xOffs,
            (int) (yOffs + boundsRect.height * scale) - (int) yOffs);
      if (!rect.contains(x, y)) {
         return;
      }
      out.add(bounds);
      for (Bounds element : bounds.children) {
         findComponents(x, y, xOffs, yOffs, scale, element, level + 1, out);
      }
   }


   /** Gets a text description of the components at a certain position.
    *
    * @param x x position of mouse.
    *
    * @param y y position of mouse.
    *
    * @param xOffset x offset from parent position.
    *
    * @param yOffset y offset from parent position.
    *
    * @param scale scale multiplier from bounds units to graphics units.
    *
    * @param bounds bounds of the component that is being searched.
    *
    * @param level the depth in the hierarchy.
    *
    * @param textOut the text result. **/
   private static void findComponents(final int x, final int y, final double xOffset, final double yOffset,
         final double scale, final Bounds bounds, final int level, final StringBuilder textOut) {
      Rectangle boundsRect = bounds.bounds;
   
      double xOffs = xOffset;
      double yOffs = yOffset;
      if (level > 0) {
         xOffs += boundsRect.x * scale;
         yOffs += boundsRect.y * scale;
      }
   
      Rectangle rect = new Rectangle((int) xOffs, (int) yOffs, (int) (xOffs + boundsRect.width * scale) - (int) xOffs,
            (int) (yOffs + boundsRect.height * scale) - (int) yOffs);
      if (!rect.contains(x, y)) {
         return;
      }
      textOut.append(" ".repeat(Math.max(0, level)));
      textOut.append(bounds.type);
      textOut.append(" (");
      textOut.append(boundsRect.x);
      textOut.append(", ");
      textOut.append(boundsRect.y);
      textOut.append(") ");
      textOut.append(boundsRect.width);
      textOut.append("x");
      textOut.append(boundsRect.height);
      textOut.append("\n");
      for (Bounds element : bounds.children) {
         findComponents(x, y, xOffs, yOffs, scale, element, level + 1, textOut);
      }
   }


   /** Gets component boundary information.
    *
    * @param parent the parent bounds, or null if there is no parent.
    *
    * @param value the component.
    *
    * @param context the current debugger context.
    *
    * @param selOut if the selected bounds are found, the first element of this array will be set to it.
    *
    * @return boundary information for the component.
    *
    * @throws ViewerException if an error occurs while retrieving the bounds. **/
   private Bounds getBounds(final Bounds parent, final Value value, final DebugContext context,
         final Bounds[] selOut) throws ViewerException {
      Bounds newBounds = new Bounds();
      Method getBoundsMethod = value.getMethod(context, "getBounds", "java.awt.Rectangle", null);
      Value bounds = value.invokeMethod(context, getBoundsMethod, null);
      String type = value.getType(context).getName(context);
      int dot = type.lastIndexOf('.');
      if (dot >= 0) {
         type = type.substring(dot + 1);
      }
      newBounds.type = type;
      newBounds.value = value;
      newBounds.id = value.getUniqueID();
      newBounds.parent = parent;
      
      if (value.equals(newSelComponent)) {
         selOut[0] = newBounds;
      }
   
      try {
         Method getTextMethod = value.getMethod(context, "getText", "java.lang.String", null);
         Value result = value.invokeMethod(context, getTextMethod, null);
         if (!result.isNull(context)) {
            String str = result.toString(context);
            if (str.length() > 10) {
               str = str.substring(0, 7) + "...";
            }
            newBounds.type += " \"" + str + "\"";
         }
      }
      catch (ViewerException ignored) {
      }
   
      newBounds.bounds.x = bounds.getFieldValue(context, "x").toInt(context);
      newBounds.bounds.y = bounds.getFieldValue(context, "y").toInt(context);
      newBounds.bounds.width = bounds.getFieldValue(context, "width").toInt(context);
      newBounds.bounds.height = bounds.getFieldValue(context, "height").toInt(context);
      if (value.isInstanceOf(context, "java.awt.Container")) {
         Method getComponentsMethod = value.getMethod(context, "getComponents", "java.awt.Component[]", null);
         Value children;
         try {
            children = value.invokeMethod(context, getComponentsMethod, null);
         }
         catch (ViewerException e) {
            newBounds.children = new Bounds[0];
            return newBounds;
         }
         newBounds.children = new Bounds[children.getArrayLength(context)];
         List<Value> childValues = children.getArrayElements(context);
         for (int childInd = 0; childInd < newBounds.children.length; childInd++) {
            newBounds.children[childInd] = getBounds(newBounds, childValues.get(childInd), context, selOut);
         }
      }
      return newBounds;
   }


   /** Paints the bounds of a component and its children.
    *
    * @param g graphics on which the rendering will be performed.
    *
    * @param xOffset x offset of the parent.
    *
    * @param yOffset y offset of the parent.
    *
    * @param scale scale multiplier from bounds units to graphics units.
    *
    * @param bounds bounds of the component that is being painted.
    *
    * @param level the depth in the hierarchy.
    *
    * @param lw line thickness for painting.
    *
    * @param selRectOut the drawing bounds of the selected component, if any, are returned in the first element of
    * this array. **/
   private void paintBounds(final Graphics2D g, final double xOffset, final double yOffset, final double scale,
         final Bounds bounds, final int level, final int lw, final Rectangle[] selRectOut) {
      Rectangle boundsRect = bounds.bounds;
   
      double xOffs = xOffset;
      double yOffs = yOffset;
      if (level > 0) {
         xOffs += boundsRect.x * scale;
         yOffs += boundsRect.y * scale;
      }
   
      g.setColor(Drawing.getColor(Colors.BLACK));
      Rectangle rect = new Rectangle((int) xOffs, (int) yOffs, (int) (xOffs + boundsRect.width * scale) - (int) xOffs,
            (int) (yOffs + boundsRect.height * scale) - (int) yOffs);
      javaToDrawing(rect);
      g.fill(rect);
      if (rect.width > lw + lw && rect.height > lw + lw) {
         double shade = Math.max(0, 1 - 0.05 * level);
         int rMin = 100;
         int rMax = 200;
         int i = (int) (rMin + shade * (rMax - rMin));
         Color color = bounds.id == selId ? new Color(255, i, i) : new Color(i, i, 255);
         g.setColor(Drawing.convertForTheme(color));
         rect.x += lw;
         rect.y += lw;
         rect.width -= 2 * lw;
         rect.height -= 2 * lw;
         g.fill(rect);
         for (Bounds element : bounds.children) {
            paintBounds(g, xOffs, yOffs, scale, element, level + 1, lw, selRectOut);
         }
      }
      if (bounds.id == selId) {
         selRectOut[0] = rect;
      }
   }


   /** Sets the text.
    *
    * @param text the new text. **/
   private void setTextAreaText(final String text) {
      if (text.equals(textArea.getText())) {
         return;
      }
      textArea.setText(text);
   }
   
   
   /** Sets the selected component.
    *
    * @param bounds bounds of the selected component. **/
   private void setSelComponent(final Bounds bounds) {
      newSelComponent = (bounds == null) ? null : bounds.value;
      getVIData().update();
   }
   
   
   /** Selects, or changes the selection.
    *
    * @param dir the action and direction of change, 0 for select/unselect, -1 for left, 1 for right, -2 for up, 2 for
    * down.
    *
    * @param block true if the amount of change should be a block, false if it should be one unit.
    *
    * @param expand true if the selection should be expanded, false if it should not. **/
   private void keyMove(final int dir, final boolean block, final boolean expand) {
      if (dir == 0) {
         if (topBounds == null) {
            return;
         }
         if (expand == (selBounds == null)) {
            return;
         }
         newSelComponent = (selBounds == null) ? topBounds.value : null;
         getVIData().update();
         return;
      }
      
      if (selBounds == null) {
         return;
      }
      
      if (dir == -2) {
         if (selBounds.parent == null) {
            return;
         }
         newSelComponent = selBounds.parent.value;
      }
      else if (dir == 2) {
         if (selBounds.children.length == 0) {
            return;
         }
         newSelComponent = selBounds.children[0].value;
      }
      else {
         if (selBounds.parent == null) {
            return;
         }
         int ind = 0;
         while (ind < selBounds.parent.children.length && selBounds.parent.children[ind].id != selBounds.id) {
            ind++;
         }
         if (ind == selBounds.parent.children.length) {
            return;
         }
         ind = (ind + selBounds.parent.children.length + dir) % selBounds.parent.children.length;
         newSelComponent = selBounds.parent.children[ind].value;
      }
      getVIData().update();
   }
}
