/* Copyright (C) 1999-2004 by Peter Eastman

   This program is free software; you can redistribute it and/or modify it under the
   terms of the GNU General Public License as published by the Free Software
   Foundation; either version 2 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 General Public License for more details. */

package artofillusion;

import artofillusion.animation.*;
import artofillusion.math.*;
import artofillusion.object.*;
import artofillusion.object.Mesh.*;
import artofillusion.object.TriangleMesh.*;
import artofillusion.texture.*;
import artofillusion.ui.*;
import artofillusion.view.*;
import buoy.event.*;
import buoy.widget.*;
import java.awt.*;
import java.util.*;

/** The TriMeshViewer class is a component which displays a TriangleMesh object and 
    allow the user to edit it. */

public class TriMeshViewer extends MeshViewer
{
  private boolean visible[], hideVert[], hideEdge[], hideFace[], draggingSelectionBox, dragging, tolerant, showQuads; // selected[]
  // int selectMode;
  private Point screenVert[];
  private double screenZ[];
  private int boundary[][];
  private TextureParameter hideFaceParam;

  public static final int POINT_MODE = 0;
  public static final int EDGE_MODE = 1;
  public static final int FACE_MODE = 2;

  public TriMeshViewer(ObjectInfo obj, RowContainer p)
  {
    super(obj, p);
    TriangleMesh mesh = (TriangleMesh) obj.object;
    visible = new boolean [mesh.getVertices().length];
    hideVert = new boolean [mesh.getVertices().length];
    findQuads();
    ((TriangleMesh)obj.getObject3D()).getSkeleton().getModelEvent()
        .addListener(this);
    // findSelectionDistance();
  }
  
  public void dispose()
  {
    if (theObject != null)
      ((TriangleMesh)theObject).getSkeleton().getModelEvent()
          .removeListener(this);
    super.dispose();
  }
  
  protected boolean handleModelEvent(ModelEvent event)
  {
    if (!super.handleModelEvent(event))
    {
      if (event.getSource() == ((TriangleMesh)theObject).getSkeleton())
      {
        if (event.hasKey("skeleton changed"))
        {
          if (getSkeletonDetached() && draggingAutosync)  // TODO(MB) Perhaps change event concept to remove this
          {
            updateInternalState();
            if (isVisible())
            {
              updateImage();
              repaint();
            }
          }
        }
        else
          return false;
      }
      else
        return false;
    }
    return true;
  }

  protected void drawObject(Graphics g)
  {
    MeshVertex v[] = ((Mesh) theObject).getVertices();
    Vec2 p[];
    
    int selectMode = getSelectionMode();

    // Calculate the screen coordinates of every vertex.

    screenVert = new Point [v.length];
    screenZ = new double [v.length];
    if (visible.length != v.length)
      visible = new boolean [v.length];
    p = new Vec2 [v.length];
    double clipDist = (theCamera.isPerspective() ? theCamera.getClipDistance() : -Double.MAX_VALUE);
    for (int i = 0; i < v.length; i++)
      {
	p[i] = theCamera.getObjectToScreen().timesXY(v[i].r);
	screenVert[i] = new Point((int) p[i].x, (int) p[i].y);
	screenZ[i] = theCamera.getObjectToView().timesZ(v[i].r);
	visible[i] = (!hideVert[i] && screenZ[i] > clipDist);
      }

    // Now draw the object.

    drawSurface(g);
    Color meshColor, selectedColor;
    if (currentTool instanceof SkeletonTool)
      {
        meshColor = Color.gray;
        selectedColor = new Color(255, 127, 255);
      }
    else
      {
        meshColor = Color.black;
        selectedColor = Color.magenta;
        if (showSkeleton && theObject.getSkeleton() != null)
          theObject.getSkeleton().draw(this, false);
      }
    if (selectMode == POINT_MODE)
      {
        drawEdges(g, p, Color.gray, Color.gray);
        drawVertices(g, meshColor, currentTool.hilightSelection() ? selectedColor : meshColor);
      }
    else
      drawEdges(g, p, meshColor, currentTool.hilightSelection() ? selectedColor : meshColor);
    if (currentTool instanceof SkeletonTool)
      if (showSkeleton && theObject.getSkeleton() != null)
        theObject.getSkeleton().draw(this, true);
  }

  /** Draw the surface of the object. */

  private void drawSurface(Graphics g)
  {
    if (!showSurface)
      return;
    boolean hide[] = null;
    if (hideFaceParam != null)
    {
      RenderingMesh mesh = objInfo.getPreviewMesh();
      double param[] = ((FaceParameterValue) mesh.param[mesh.param.length-1]).getValue();
      hide = new boolean [param.length];
      for (int i = 0; i < hide.length; i++)
        hide[i] = (param[i] == 1.0);
    }
    if (renderMode == RENDER_WIREFRAME)
      {
        g.setColor(surfaceColor);
        Object3D.draw(g, theCamera, objInfo.getWireframePreview(), objInfo.getBounds());
      }
    else if (renderMode == RENDER_TRANSPARENT)
      renderMeshTransparent(objInfo.getPreviewMesh(), theCamera, theCamera.getViewToWorld().timesDirection(Vec3.vz()), surfaceRGB, hide);
    else
    {
      RenderingMesh mesh = objInfo.getPreviewMesh();
      Vec3 viewDir = theCamera.getViewToWorld().timesDirection(Vec3.vz());
      VertexShader shader;
      if (renderMode == RENDER_FLAT)
        shader = new FlatVertexShader(mesh, surfaceRGB, viewDir);
      else if (renderMode == RENDER_SMOOTH)
        shader = new SmoothVertexShader(mesh, surfaceRGB, viewDir);
      else
        shader = new TexturedVertexShader(mesh, objInfo.object, 0.0, viewDir).optimize();
      renderMesh(mesh, shader, theCamera, objInfo.object.isClosed(), hide);
    }
  }

  /** Draw the vertices of the control mesh. */

  private void drawVertices(Graphics g, Color unselectedColor, Color selectedColor)
  {
    if (!showMesh)
      return;
    MeshVertex v[] = ((Mesh) theObject).getVertices();
    boolean[] selected = getSelection();

    // First, draw any unselected portions of the object.

    if (renderMode == RENDER_WIREFRAME || renderMode == RENDER_TRANSPARENT)
      {
        for (int i = 0; i < v.length; i++)
          if (!selected[i] && visible[i])
            drawBox(screenVert[i].x-HANDLE_SIZE/2, screenVert[i].y-HANDLE_SIZE/2, HANDLE_SIZE, HANDLE_SIZE, unselectedColor);
      }
    else
      {
        for (int i = 0; i < v.length; i++)
          if (!selected[i] && visible[i])
            renderBox(screenVert[i].x-HANDLE_SIZE/2, screenVert[i].y-HANDLE_SIZE/2, HANDLE_SIZE, HANDLE_SIZE, screenZ[i]-0.01, unselectedColor);
      }

    // Now draw the selected portions.

    if (renderMode == RENDER_WIREFRAME || renderMode == RENDER_TRANSPARENT)
      {
        for (int i = 0; i < v.length; i++)
          if (selected[i] && visible[i])
            drawBox(screenVert[i].x-HANDLE_SIZE/2, screenVert[i].y-HANDLE_SIZE/2, HANDLE_SIZE, HANDLE_SIZE, selectedColor);
      }
    else
      {
        for (int i = 0; i < v.length; i++)
          if (selected[i] && visible[i])
            renderBox(screenVert[i].x-HANDLE_SIZE/2, screenVert[i].y-HANDLE_SIZE/2, HANDLE_SIZE, HANDLE_SIZE, screenZ[i]-0.01, selectedColor);
      }
  }

  /** Draw the edges of the control mesh. */

  private void drawEdges(Graphics g, Vec2 p[], Color unselectedColor, Color selectedColor)
  {
    if (!showMesh)
      return;
    Edge e[] = ((TriangleMesh) theObject).getEdges();

    int selectMode = getSelectionMode();
    boolean[] selected = getSelection();
    
    // First, draw any unselected portions of the object.

    if (renderMode == RENDER_WIREFRAME || renderMode == RENDER_TRANSPARENT)
      {
        if (selectMode == POINT_MODE)
          {
            for (int i = 0; i < e.length; i++)
              if (!hideEdge[i] && visible[e[i].v1] && visible[e[i].v2])
                drawLine(screenVert[e[i].v1], screenVert[e[i].v2], unselectedColor);
          }
        else if (selectMode == EDGE_MODE)
          {
            for (int i = 0; i < e.length; i++)
              if (!selected[i] && !hideEdge[i] && visible[e[i].v1] && visible[e[i].v2])
                drawLine(screenVert[e[i].v1], screenVert[e[i].v2], unselectedColor);
          }
        else
          {
            for (int i = 0; i < e.length; i++)
              if (!hideEdge[i] && visible[e[i].v1] && visible[e[i].v2] && !selected[e[i].f1] && (e[i].f2 == -1 || !selected[e[i].f2]))
                drawLine(screenVert[e[i].v1], screenVert[e[i].v2], unselectedColor);
          }
      }
    else
      {
        if (selectMode == POINT_MODE)
          {
            for (int i = 0; i < e.length; i++)
              if (!hideEdge[i] && visible[e[i].v1] && visible[e[i].v2])
                renderLine(p[e[i].v1], screenZ[e[i].v1]-0.01, p[e[i].v2], screenZ[e[i].v2]-0.01, theCamera, unselectedColor);
          }
        else if (selectMode == EDGE_MODE)
          {
            for (int i = 0; i < e.length; i++)
              if (!selected[i] && !hideEdge[i] && visible[e[i].v1] && visible[e[i].v2])
                renderLine(p[e[i].v1], screenZ[e[i].v1]-0.01, p[e[i].v2], screenZ[e[i].v2]-0.01, theCamera, unselectedColor);
          }
        else
          {
            for (int i = 0; i < e.length; i++)
//              if (!hideEdge[i] && visible[e[i].v1] && visible[e[i].v2] && !selected[e[i].f1] && (e[i].f2 == -1 || !selected[e[i].f2]))
                renderLine(p[e[i].v1], screenZ[e[i].v1]-0.01, p[e[i].v2], screenZ[e[i].v2]-0.01, theCamera, unselectedColor);
          }
      }

    // Now draw the selected portions.

    if (renderMode == RENDER_WIREFRAME || renderMode == RENDER_TRANSPARENT)
      {
        if (selectMode == EDGE_MODE)
          {
            for (int i = 0; i < e.length; i++)
              if (selected[i] && !hideEdge[i] && visible[e[i].v1] && visible[e[i].v2])
                drawLine(screenVert[e[i].v1], screenVert[e[i].v2], selectedColor);
          }
        else if (selectMode == FACE_MODE)
          {
            for (int i = 0; i < e.length; i++)
              if (!hideEdge[i] && visible[e[i].v1] && visible[e[i].v2] && (selected[e[i].f1] || (e[i].f2 != -1 && selected[e[i].f2])))
                drawLine(screenVert[e[i].v1], screenVert[e[i].v2], selectedColor);
          }
      }
    else
      {
        if (selectMode == EDGE_MODE)
          {
            for (int i = 0; i < e.length; i++)
              if (selected[i] && !hideEdge[i] && visible[e[i].v1] && visible[e[i].v2])
                renderLine(p[e[i].v1], screenZ[e[i].v1]-0.01, p[e[i].v2], screenZ[e[i].v2]-0.01, theCamera, selectedColor);
          }
        else if (selectMode == FACE_MODE)
          {
            for (int i = 0; i < e.length; i++)
              if (!hideEdge[i] && visible[e[i].v1] && visible[e[i].v2] && (selected[e[i].f1] || (e[i].f2 != -1 && selected[e[i].f2])))
                renderLine(p[e[i].v1], screenZ[e[i].v1]-0.01, p[e[i].v2], screenZ[e[i].v2]-0.01, theCamera, selectedColor);
          }
      }
  }
  
  
  // TODO(MB) The face visibility should NOT be a property of the mesh
  // but of the viewer or the editor window
  
  /** Add an extra texture parameter to the mesh which will be used for keeping track of which
      faces are hidden. */
  
  private void addExtraParameter()
  {
    if (hideFaceParam != null)
      return;
    hideFaceParam = new TextureParameter(this, "Hide Face", 0.0, 1.0, 0.0);
    TextureParameter params[] = theObject.getParameters();
    TextureParameter newparams[] = new TextureParameter [params.length+1];
    ParameterValue values[] = theObject.getParameterValues();
    ParameterValue newvalues[] = new ParameterValue [values.length+1];
    for (int i = 0; i < params.length; i++)
    {
      newparams[i] = params[i];
      newvalues[i] = values[i];
    }
    newparams[params.length] = hideFaceParam;
    newvalues[values.length] = new FaceParameterValue((TriangleMesh) theObject, hideFaceParam);
    theObject.setParameters(newparams);
    theObject.setParameterValues(newvalues);
    objInfo.clearCachedMeshes();
  }

  /** Remove the extra texture parameter from the mesh which was used for keeping track of which
      faces are hidden. */
  
  public void removeExtraParameter()
  {
    if (hideFaceParam == null)
      return;
    hideFaceParam = null;
    TextureParameter params[] = theObject.getParameters();
    TextureParameter newparams[] = new TextureParameter [params.length-1];
    ParameterValue values[] = theObject.getParameterValues();
    ParameterValue newvalues[] = new ParameterValue [values.length-1];
    for (int i = 0; i < newparams.length; i++)
    {
      newparams[i] = params[i];
      newvalues[i] = values[i];
    }
    theObject.setParameters(newparams);
    theObject.setParameterValues(newvalues);
    objInfo.clearCachedMeshes();
  }

  /** Get the extra texture parameter which was added the mesh to keep track of which
      faces are hidden. */
  
  public TextureParameter getExtraParameter()
  {
    return hideFaceParam;
  }

  /** Draw any parts of the mesh which have been dragged. */

  public void drawDraggedSelection(Graphics g, Camera cam, Vec3[] v, boolean broadcast)
  {
    Vertex vert[] = (Vertex []) ((TriangleMesh) theObject).getVertices();
    Edge edge[] = ((TriangleMesh) theObject).getEdges();
    int selectMode = getSelectionMode();

    if (selectMode == POINT_MODE)
      for (int i = 0; i < vert.length; i++)
        if (vert[i].r != v[i])
          {
            Vec2 p = cam.getObjectToScreen().timesXY(v[i]);
            g.fillRect(((int) p.x) - HANDLE_SIZE/2, ((int) p.y) - HANDLE_SIZE/2, HANDLE_SIZE, HANDLE_SIZE);
          }
    for (int i = 0; i < edge.length; i++)
      if (!hideEdge[i] && (vert[edge[i].v1].r != v[edge[i].v1] || vert[edge[i].v2].r != v[edge[i].v2]))
	cam.drawClippedLine(g, v[edge[i].v1], v[edge[i].v2]);
    
    broadcastDraggedSelection(v, broadcast);
  }

  /** When the selection mode changes, do our best to convert the old selection to the 
      new mode. */

  public void setSelectionMode(int mode)
  {
    Vertex v[] = (Vertex []) ((TriangleMesh) theObject).getVertices();
    Edge e[] = ((TriangleMesh) theObject).getEdges();
    Face f[] = ((TriangleMesh) theObject).getFaces();
    boolean newSel[];
    int i;

    int selectMode = getSelectionMode();
    boolean[] selected = getSelection();

    if (mode == selectMode)
      return;
    if (mode == POINT_MODE)
      {
	newSel = new boolean [v.length];
	if (selectMode == EDGE_MODE)
	  {
	    for (i = 0; i < e.length; i++)
	      if (selected[i])
		newSel[e[i].v1] = newSel[e[i].v2] = true;
	  }
	else
	  {
	    for (i = 0; i < f.length; i++)
	      if (selected[i])
		newSel[f[i].v1] = newSel[f[i].v2] = newSel[f[i].v3] = true;
	  }
      }
    else if (mode == EDGE_MODE)
      {
	newSel = new boolean [e.length];
	if (selectMode == POINT_MODE)
	  {
	    if (tolerant)
	      for (i = 0; i < e.length; i++)
		newSel[i] = (!hideEdge[i] && (selected[e[i].v1] || selected[e[i].v2]));
	    else
	      for (i = 0; i < e.length; i++)
		newSel[i] = (!hideEdge[i] && (selected[e[i].v1] && selected[e[i].v2]));
	  }
	else
	  {
	    for (i = 0; i < f.length; i++)
	      if (selected[i])
		newSel[f[i].e1] = newSel[f[i].e2] = newSel[f[i].e3] = true;
	  }
      }
    else
      {
	newSel = new boolean [f.length];
	if (selectMode == POINT_MODE)
	  {
	    if (tolerant)
	      for (i = 0; i < f.length; i++)
		newSel[i] = (selected[f[i].v1] || selected[f[i].v2] || selected[f[i].v3]);
	    else
	      for (i = 0; i < f.length; i++)
		newSel[i] = (selected[f[i].v1] && selected[f[i].v2] && selected[f[i].v3]);
	  }
	else
	  {
	    for (i = 0; i < f.length; i++)
	      newSel[i] = (selected[f[i].e1] && selected[f[i].e2] && selected[f[i].e3]);
	  }
      }
    
    getSelectionHolder().setSelectionMode(mode);
    getSelectionHolder().setSelection(newSel);
    //?? getSelectionHolder().informChanged(this, ModelEvent.CHANGEDUR_OBJECTEDITOR);
  }

//  protected boolean handleModelEvent(ModelEvent event)
//  {
//    if (!super.handleModelEvent(event))
//    {
//      if (event.getSource() == selHolder)
//      {
//         updateImage();
//         repaint();
//      }
//      else
//        return false;
//    }
//    return true;
//  }
  
  /** Get an array of flags telling which vertices of the mesh are
    currently visible. */

  public boolean[] getVisible()
  {
    return visible;
  }
  
  public Point[] getScreenVert()
  {
    return screenVert;
  }
  
  /** Determine whether we are in tolerant selection mode. */

  public boolean isTolerant()
  {
    return tolerant;
  }

  /** Set whether to use tolerant selection mode. */

  public void setTolerant(boolean tol)
  {
    tolerant = tol;
  }

  /** Determine whether the mesh is being displayed as quads. */

  public boolean isQuadMode()
  {
    return showQuads;
  }

  /** Set whether to display the mesh as quads. */

  public void setQuadMode(boolean quads)
  {
    showQuads = quads;
    findQuads();
    findSelectionDistance();
  }

  /** Determine whether a particular edge is hidden to simulate a quad. */

  public boolean isEdgeHidden(int which)
  {
    return hideEdge[which];
  }

  /** Get which faces are hidden.  This may be null, which means that all faces are visible. */
  
  public boolean [] getHiddenFaces()
  {
    return hideFace;
  }
  
  /** Set which faces are hidden.  Pass null to show all faces. */
  // TODO(MB) Why should the viewer be responsible for this?
  public void setHiddenFaces(boolean hidden[])
  {
    hideFace = hidden;
    TriangleMesh obj = (TriangleMesh) theObject;
    hideVert = new boolean [obj.getVertices().length];
    if (hideFace != null)
    {
      for (int i = 0; i < hideVert.length; i++)
        hideVert[i] = true;
      TriangleMesh.Face face[] = obj.getFaces();
      for (int i = 0; i < face.length; i++)
        if (!hideFace[i])
          hideVert[face[i].v1] = hideVert[face[i].v2] = hideVert[face[i].v3] = false;
      addExtraParameter();
      FaceParameterValue val = (FaceParameterValue) theObject.getParameterValue(hideFaceParam);
      double param[] = val.getValue();
      for (int i = 0; i < hideFace.length; i++)
        param[i] = (hideFace[i] ? 1.0 : 0.0);
      val.setValue(param);
      theObject.setParameterValue(hideFaceParam, val);
      objInfo.clearCachedMeshes();
    }
    else
    {
      removeExtraParameter();
      for (int i = 0; i < hideVert.length; i++)
        hideVert[i] = false;
    }
    findQuads();
    updateImage();
    repaint();
  }
  
  /** Sets the selection in the current SelectionHolder. Does not fire an
    update event for the selection. */

  public void setSelection(boolean sel[])
  {
    getSelectionHolder().setSelection(sel);
    findSelectionDistance();
    currentTool.getWindow().updateMenus();
    updateImage();
    repaint();
  }

  /** Calculate the distance (in edges) between each vertex and the nearest selected vertex. */

  protected void findSelectionDistance()
  {
    int i, j;
    int dist[] = new int [((TriangleMesh) theObject).getVertices().length];
    Edge e[] = ((TriangleMesh) theObject).getEdges();
    Face f[] = ((TriangleMesh) theObject).getFaces();
    int selectMode = getSelectionMode();
    boolean[] selected = getSelection();

    maxDistance = TriMeshEditorWindow.getTensionDistance();

    // First, set each distance to 0 or -1, depending on whether that vertex is part of the
    // current selection.

    if (selectMode == POINT_MODE)
      for (i = 0; i < dist.length; i++)
	dist[i] = selected[i] ? 0 : -1;
    else if (selectMode == EDGE_MODE)
      {
	for (i = 0; i < dist.length; i++)
	  dist[i] = -1;
	for (i = 0; i < selected.length; i++)
	  if (selected[i])
	    dist[e[i].v1] = dist[e[i].v2] = 0;
      }
    else
      {
	for (i = 0; i < dist.length; i++)
	  dist[i] = -1;
	for (i = 0; i < selected.length; i++)
	  if (selected[i])
	    dist[f[i].v1] = dist[f[i].v2] = dist[f[i].v3] = 0;
      }

    // Now extend this outward up to maxDistance.

    for (i = 0; i < maxDistance; i++)
      for (j = 0; j < e.length; j++)
	{
          if (hideEdge[j])
            continue;
	  if (dist[e[j].v1] == -1 && dist[e[j].v2] == i)
	    dist[e[j].v1] = i+1;
	  else if (dist[e[j].v2] == -1 && dist[e[j].v1] == i)
	    dist[e[j].v2] = i+1;
	}
    selectionDistance = dist;
  }
  
  
  public void extendDistance(int dist[], int maxDistance)
  {
    int i, j;
    Edge e[] = ((TriangleMesh) theObject).getEdges();
    Face f[] = ((TriangleMesh) theObject).getFaces();
    boolean canStop;
    
    for (i = 0; (maxDistance == -1) || (i < maxDistance); ++i)
    {
      canStop = true;
      for (j = 0; j < e.length; j++)
      {
        if (hideEdge[j])
          continue;
        if (dist[e[j].v1] == -1 && dist[e[j].v2] == i)
        {
          dist[e[j].v1] = i+1;
          canStop = false;
        }
        else if (dist[e[j].v2] == -1 && dist[e[j].v1] == i)
        {
          dist[e[j].v2] = i+1;
          canStop = false;
        }
      }
      if (canStop)
        break;
    }
  }

  public void setMesh(Mesh mesh)
  {
    TriangleMesh obj = (TriangleMesh) mesh;
    //System.out.println("TriMeshViewer setMesh1 "+obj.getVertices().length);
    setObject(obj);
    int selectMode = getSelectionMode();
//    boolean[] selected = getSelection();

    //System.out.println("TriMeshViewer setMesh2 "+obj.getVertices().length);
    int newsellength = -1;
    if (selectMode == POINT_MODE)
      newsellength = obj.getVertices().length;
    else if (selectMode == EDGE_MODE)
      newsellength = obj.getEdges().length;
    else if (selectMode == FACE_MODE)
      newsellength = obj.getFaces().length;
      
    getSelectionHolder().setSelection(
        Utilities.arrayEnsureLength(getSelection(), newsellength));
    visible = Utilities.arrayEnsureLength(visible, newsellength);
      
    hideVert = new boolean [obj.getVertices().length];
    if (hideFaceParam != null)
    {
      FaceParameterValue val = (FaceParameterValue) theObject.getParameterValue(hideFaceParam);
      double param[] = val.getValue();
      hideFace = new boolean [param.length];
      for (int i = 0; i < param.length; i++)
        hideFace[i] = (param[i] == 1.0);
    }
    findQuads();
    findSelectionDistance();
    boundary = null;
    currentTool.getWindow().updateMenus();
  }

  /** Get arrays of the indices of all edges which form boundaries that have been selected. */

  public int [][] findSelectedBoundaries()
  {
    
    if (getSelectionMode() != EDGE_MODE)
      return new int [0][0];
    if (boundary == null)
      boundary = ((TriangleMesh) theObject).findBoundaryEdges();
    Vector all = new Vector();
    for (int i = 0; i < boundary.length; i++)
    {
      // Add one "selected boundary" for every continuous run of selected edges.
      
      int start;
      for (start = boundary[i].length-1; start > 0 && getSelection()[boundary[i][start]]; start--);
      Vector current = null;
      int j = start;
      do
      {
        boolean isSelected = getSelection()[boundary[i][j]];
        if (isSelected)
        {
          if (current == null)
            current = new Vector();
          current.addElement(new Integer(boundary[i][j]));
        }
        if (++j == boundary[i].length)
          j = 0;
        if ((!isSelected || j == start) && current != null)
        {
          int edgeList[] = new int [current.size()];
          for (int k = 0; k < edgeList.length; k++)
            edgeList[k] = ((Integer) current.elementAt(k)).intValue();
          all.addElement(edgeList);
          current = null;
        }
      } while (j != start);
    }
    int index[][] = new int [all.size()][];
    all.copyInto(index);
    return index;
  }

  /** When the object changes, we need to rebuild the quad display. */

  public void objectChanged()
  {
    super.objectChanged();
    findQuads();
  }


  /** When the user presses the mouse, forward events to the current tool as appropriate.
      If this is a vertex based tool, allow them to select or deselect vertices. */

  protected void mousePressed(WidgetMouseEvent e)
  {
    Vertex v[] = (Vertex []) ((TriangleMesh) theObject).getVertices();
    Edge ed[] = ((TriangleMesh) theObject).getEdges();
    Face f[] = ((TriangleMesh) theObject).getFaces();
    int selectMode = getSelectionMode();
    boolean[] selected = getSelection();
    int i, j, k;

    requestFocus();
    sentClick = true;
    deselect = -1;
    dragging = false;
    clickPoint = e.getPoint();

    // Determine which tool is active.

    if (metaTool != null && e.isMetaDown())
      activeTool = metaTool;
    else if (altTool != null && e.isAltDown())
      activeTool = altTool;
    else
      activeTool = currentTool;

    // If the current tool wants all clicks, just forward the event and return.

    if (activeTool.whichClicks() == EditingTool.ALL_CLICKS)
      {
	activeTool.mousePressed(e, this);
	dragging = true;
	return;
      }

    // Determine what the click was on.

    i = findClickTarget(e.getPoint(), null);

    // If the click was not on an object, start dragging a selection box.

    if (i == -1)
      {
	draggingSelectionBox = true;
	beginDraggingSelection(e.getPoint(), false);
	sentClick = false;
	return;
      }

    // If we are in edge or face selection mode, find a vertex of the clicked edge or face,
    // so that it can be passed to editing tools.

    if (selectMode == EDGE_MODE)
      {
	if (visible[ed[i].v1])
	  j = ed[i].v1;
	else
	  j = ed[i].v2;
      }
    else if (selectMode == FACE_MODE)
      {
	if (visible[f[i].v1])
	  j = f[i].v1;
	else if (visible[f[i].v2])
	  j = f[i].v2;
	else
	  j = f[i].v3;
      }
    else
      j = i;

    // If the click was on a selected object, forward it to the current tool.  If it was a
    // shift-click, the user may want to deselect it, so set a flag.

    if (selected[i])
      {
	if (e.isShiftDown())
	  deselect = i;
	activeTool.mousePressedOnHandle(e, this, 0, j);
	return;
      }

    // The click was on an unselected object.  Select it and send an event to the current tool.

    if (!e.isShiftDown())
      for (k = 0; k < selected.length; k++)
	selected[k] = false;
    selected[i] = true;
    if (selectMode == FACE_MODE)
      {
        if (hideEdge[f[i].e1])
          selected[ed[f[i].e1].f1] = selected[ed[f[i].e1].f2] = true;
        if (hideEdge[f[i].e2])
          selected[ed[f[i].e2].f1] = selected[ed[f[i].e2].f2] = true;
        if (hideEdge[f[i].e3])
          selected[ed[f[i].e3].f1] = selected[ed[f[i].e3].f2] = true;
      }
    getSelectionHolder().informChanged(this, ModelEvent.CHANGEDUR_OBJECTEDITOR);
    findSelectionDistance();
    currentTool.getWindow().updateMenus();
    if (e.isShiftDown())
      {
	sentClick = false;
	updateImage();
	repaint();
      }
    else
      activeTool.mousePressedOnHandle(e, this, 0, j);
  }

  protected void mouseDragged(WidgetMouseEvent e)
  {
    if (!dragging)
      {
	Point p = e.getPoint();
	if (Math.abs(p.x-clickPoint.x) < 2 && Math.abs(p.y-clickPoint.y) < 2)
	  return;
      }
    dragging = true;
    deselect = -1;
    super.mouseDragged(e);
  }

  protected void mouseReleased(WidgetMouseEvent e)
  {
    Edge ed[] = ((TriangleMesh) theObject).getEdges();
    Face fc[] = ((TriangleMesh) theObject).getFaces();
    int selectMode = getSelectionMode();
    boolean[] selected = getSelection();

    Rectangle r;
    int i, j, x, y;
    Vec2 pos;

    moveToGrid(e);
    endDraggingSelection();
    if (draggingSelectionBox && !e.isShiftDown() && !e.isControlDown())
      for (i = 0; i < selected.length; i++)
        selected[i] = false;

    // If the user was dragging a selection box, then select or deselect anything
    // it intersects.

    if (selectBounds != null)
      {
	boolean newsel = !e.isControlDown();
	if (selectMode == POINT_MODE)
	  {
	    for (i = 0; i < selected.length; i++)
	    {
	      if (!hideVert[i] && selectionRegionContains(screenVert[i]))
		selected[i] = newsel;
	    }
	  }
	else if (selectMode == EDGE_MODE)
	  {
	    if (tolerant)
	      {
		for (i = 0; i < selected.length; i++)
		  if (!hideEdge[i] && (selectionRegionContains(screenVert[ed[i].v1]) || selectionRegionContains(screenVert[ed[i].v2])))
		    selected[i] = newsel;
	      }
	    else
	      {
		for (i = 0; i < selected.length; i++)
		  if (!hideEdge[i] && (selectionRegionContains(screenVert[ed[i].v1]) && selectionRegionContains(screenVert[ed[i].v2])))
		    selected[i] = newsel;
	      }
	  }
	else
	  {
	    if (tolerant)
	      {
		for (i = 0; i < selected.length; i++)
                  if (hideFace == null || !hideFace[i])
                    if (selectionRegionContains(screenVert[fc[i].v1]) || selectionRegionContains(screenVert[fc[i].v2]) || selectionRegionContains(screenVert[fc[i].v3]))
                      selected[i] = newsel;
	      }
	    else
	      {
		for (i = 0; i < selected.length; i++)
                  if (hideFace == null || !hideFace[i])
                    if (selectionRegionContains(screenVert[fc[i].v1]) && selectionRegionContains(screenVert[fc[i].v2]) && selectionRegionContains(screenVert[fc[i].v3]))
                      selected[i] = newsel;
	      }
	  }
      }
    draggingBox = draggingSelectionBox = false;

    // Send the event to the current tool, if appropriate.

    if (sentClick)
      {
	if (!dragging)
	  {
	    Point p = e.getPoint();
	    e.translatePoint(clickPoint.x-p.x, clickPoint.y-p.y);
	  }
	activeTool.mouseReleased(e, this);
      }

    // If the user shift-clicked a selected point and released the mouse without dragging,
    // then deselect the point.

    if (deselect > -1)
      {
        selected[deselect] = false;
        if (selectMode == FACE_MODE)
          {
            Face f = fc[deselect];
            if (hideEdge[f.e1])
              selected[ed[f.e1].f1] = selected[ed[f.e1].f2] = false;
            if (hideEdge[f.e2])
              selected[ed[f.e2].f1] = selected[ed[f.e2].f2] = false;
            if (hideEdge[f.e3])
              selected[ed[f.e3].f1] = selected[ed[f.e3].f2] = false;
          }
      }
    getSelectionHolder().informChanged(this, ModelEvent.CHANGEDUR_OBJECTEDITOR);
    findSelectionDistance();
    currentTool.getWindow().updateMenus();
    //updateImage();
    //repaint();
  }

  /** Determine which vertex, edge, or face (depending on the current selection mode) the
      mouse was clicked on.  If the click was on top of multiple objects, priority is given
      to ones which are currently selected, and then to ones which are in front.  If the
      click is not over any object, -1 is returned. */

  public int findClickTarget(Point pos, Vec3 uvw)
  {
    Vertex vt[] = (Vertex []) ((TriangleMesh) theObject).getVertices();
    Edge ed[] = ((TriangleMesh) theObject).getEdges();
    Face fc[] = ((TriangleMesh) theObject).getFaces();
    int selectMode = getSelectionMode();
    boolean[] selected = getSelection();
    double u, v, w, z, closestz = Double.MAX_VALUE;
    boolean sel = false;
    Point v1, v2, v3;
    int i, which = -1;

    if (selectMode == POINT_MODE)
      {
	for (i = 0; i < vt.length; i++)
	  {
	    if (!visible[i])
	      continue;
	    if (sel && !selected[i])
	      continue;
	    v1 = screenVert[i];
	    if (pos.x < v1.x-HANDLE_SIZE/2 || pos.x > v1.x+HANDLE_SIZE/2 ||
		pos.y < v1.y-HANDLE_SIZE/2 || pos.y > v1.y+HANDLE_SIZE/2)
	      continue;
	    z = theCamera.getObjectToView().timesZ(vt[i].r);
//	    if (z < closestz)
	    if ( (!sel && selected[i]) || z < closestz)  // Changed to fix original ver.
	      {
		which = i;
		closestz = z;
		sel = selected[i];
	      }
	  }
      }
    else if (selectMode == EDGE_MODE)
      {
	for (i = 0; i < ed.length; i++)
	  {
	    if (!visible[ed[i].v1] || !visible[ed[i].v2])
	      continue;
            if (hideEdge[i])
              continue;
	    if (sel && !selected[i])
	      continue;
	    v1 = screenVert[ed[i].v1];
	    v2 = screenVert[ed[i].v2];
	    if ((pos.x < v1.x-HANDLE_SIZE/2 && pos.x < v2.x-HANDLE_SIZE/2) ||
	    	(pos.x > v1.x+HANDLE_SIZE/2 && pos.x > v2.x+HANDLE_SIZE/2) ||
	    	(pos.y < v1.y-HANDLE_SIZE/2 && pos.y < v2.y-HANDLE_SIZE/2) ||
	    	(pos.y > v1.y+HANDLE_SIZE/2 && pos.y > v2.y+HANDLE_SIZE/2))
	      continue;

	    // Determine the distance of the click point from the line.

	    if (Math.abs(v1.x-v2.x) > Math.abs(v1.y-v2.y))
	      {
		if (v2.x > v1.x)
		  {
		    v = ((double) pos.x-v1.x)/(v2.x-v1.x);
		    u = 1.0-v;
		  }
		else
		  {
		    u = ((double) pos.x-v2.x)/(v1.x-v2.x);
		    v = 1.0-u;
		  }
		w = u*v1.y + v*v2.y - pos.y;
	      }
	    else
	      {
		if (v2.y > v1.y)
		  {
		    v = ((double) pos.y-v1.y)/(v2.y-v1.y);
		    u = 1.0-v;
		  }
		else
		  {
		    u = ((double) pos.y-v2.y)/(v1.y-v2.y);
		    v = 1.0-u;
		  }
		w = u*v1.x + v*v2.x - pos.x;
	      }
	    if (Math.abs(w) > HANDLE_SIZE/2)
	      continue;
	    z = u*theCamera.getObjectToView().timesZ(vt[ed[i].v1].r) +
	    	v*theCamera.getObjectToView().timesZ(vt[ed[i].v2].r);
	    if ( (!sel && selected[i]) || z < closestz)
	      {
		which = i;
		closestz = z;
		sel = selected[i];
                if (uvw != null)
                  uvw.set(u, v, w);
	      }
	  }
      }
    else
      {
	double e1x, e1y, e2x, e2y, denom, vx, vy;

	for (i = 0; i < fc.length; i++)
	  {
            if (hideFace != null && hideFace[i])
              continue;
	    if (!visible[fc[i].v1] || !visible[fc[i].v2] || !visible[fc[i].v3])
	      continue;
	    if (sel && !selected[i])
	      continue;
	    v1 = screenVert[fc[i].v1];
	    v2 = screenVert[fc[i].v2];
	    v3 = screenVert[fc[i].v3];
	    if ((pos.x < v1.x-HANDLE_SIZE/2 && pos.x < v2.x-HANDLE_SIZE/2 && pos.x < v3.x-HANDLE_SIZE/2) ||
	    	(pos.x > v1.x+HANDLE_SIZE/2 && pos.x > v2.x+HANDLE_SIZE/2 && pos.x > v3.x+HANDLE_SIZE/2) ||
	    	(pos.y < v1.y-HANDLE_SIZE/2 && pos.y < v2.y-HANDLE_SIZE/2 && pos.y < v3.y-HANDLE_SIZE/2) ||
	    	(pos.y > v1.y+HANDLE_SIZE/2 && pos.y > v2.y+HANDLE_SIZE/2 && pos.y > v3.y+HANDLE_SIZE/2))
	      continue;

	    // Determine whether the click point was inside the triangle.

	    e1x = v1.x-v2.x;
	    e1y = v1.y-v2.y;
	    e2x = v1.x-v3.x;
	    e2y = v1.y-v3.y;
	    denom = 1.0/(e1x*e2y-e1y*e2x);
	    e1x *= denom;
	    e1y *= denom;
	    e2x *= denom;
	    e2y *= denom;
	    vx = pos.x - v1.x;
	    vy = pos.y - v1.y;
	    v = e2x*vy - e2y*vx;
	    if (v < 0.0 || v > 1.0)
	      continue;
	    w = vx*e1y - vy*e1x;
	    if (w < 0.0 || w > 1.0)
	      continue;
	    u = 1.0-v-w;
	    if (u < 0.0 || u > 1.0)
	      continue;
	    z = u*theCamera.getObjectToView().timesZ(vt[fc[i].v1].r) +
		v*theCamera.getObjectToView().timesZ(vt[fc[i].v2].r) +
	    	w*theCamera.getObjectToView().timesZ(vt[fc[i].v3].r);
	    if ( (!sel && selected[i]) || z < closestz)
	      {
		which = i;
		closestz = z;
		sel = selected[i];
                if (uvw != null)
                  uvw.set(u, v, w);
	      }
	  }
      }
    return which;
  }

  /** Find edges which should be hidden to make the object seem to be made of quads. */

  private void findQuads()
  {
    Vertex v[] = (Vertex []) ((TriangleMesh) theObject).getVertices();
    Edge e[] = ((TriangleMesh) theObject).getEdges();
    Face f[] = ((TriangleMesh) theObject).getFaces();
    if (hideEdge == null || hideEdge.length != e.length)
      hideEdge = new boolean [e.length];
    if (hideFace == null)
      for (int i = 0; i < e.length; i++)
        hideEdge[i] = false;
    else
      for (int i = 0; i < e.length; i++)
        hideEdge[i] = (hideFace[e[i].f1] && (e[i].f2 == -1 || hideFace[e[i].f2]));
    if (!showQuads)
      return;

    // An edge is a candidate for hiding if the two faces it borders are in the same plane.

    boolean candidate[] = new boolean [e.length];
    Vec3 norm[] = new Vec3 [f.length];
    for (int i = 0; i < f.length; i++)
      {
	Face fc = f[i];
        norm[i] = v[fc.v2].r.minus(v[fc.v1].r).cross(v[fc.v3].r.minus(v[fc.v1].r));
	double length = norm[i].length();
	if (length > 0.0)
	  norm[i].scale(1.0/length);
      }
    for (int i = 0; i < e.length; i++)
      candidate[i] = (e[i].f2 != -1 && norm[e[i].f1].dot(norm[e[i].f2]) > 0.99);

    // Give every candidate edge a score for how close the adjoining faces are to forming
    // a rectangle.

    class EdgeScore implements Comparable
      {
        public int edge;
        public double score;

        public EdgeScore(int edge, double score)
        {
          this.edge = edge;
          this.score = score;
        }

        public int compareTo(Object o)
        {
          double diff = score-((EdgeScore) o).score;
          if (diff < 0.0)
            return -1;
          if (diff > 0.0)
            return 1;
          return 0;
        }
      }
    Vector scoreVec = new Vector(e.length);
    Vec3 temp0 = new Vec3(), temp1 = new Vec3(), temp2 = new Vec3();
    for (int i = 0; i < e.length; i++)
      {
        if (!candidate[i])
          continue;

        // Find the four vertices.

        Edge ed = e[i];
        int v1 = ed.v1, v2 = ed.v2, v3, v4;
        Face fc = f[ed.f1];
        if (fc.v1 != v1 && fc.v1 != v2)
          v3 = fc.v1;
        else if (fc.v2 != v1 && fc.v2 != v2)
          v3 = fc.v2;
        else
          v3 = fc.v3;
        fc = f[ed.f2];
        if (fc.v1 != v1 && fc.v1 != v2)
          v4 = fc.v1;
        else if (fc.v2 != v1 && fc.v2 != v2)
          v4 = fc.v2;
        else
          v4 = fc.v3;

        // Find the angles formed by them.

        temp0.set(v[v1].r.minus(v[v2].r));
        temp0.normalize();
        temp1.set(v[v1].r.minus(v[v3].r));
        temp1.normalize();
        temp2.set(v[v1].r.minus(v[v4].r));
        temp2.normalize();
        if (Math.acos(temp0.dot(temp1))+Math.acos(temp0.dot(temp2)) > Math.PI)
          continue;
        double dot = temp1.dot(temp2);
        double score = (dot > 0.0 ? dot : -dot);
        temp1.set(v[v2].r.minus(v[v3].r));
        temp1.normalize();
        temp2.set(v[v2].r.minus(v[v4].r));
        temp2.normalize();
        if (Math.acos(-temp0.dot(temp1))+Math.acos(-temp0.dot(temp2)) > Math.PI)
          continue;
        dot = temp1.dot(temp2);
        score += (dot > 0.0 ? dot : -dot);
        scoreVec.addElement(new EdgeScore(i, score));
      }
    if (scoreVec.size() == 0)
      return;

    // Sort them.

    EdgeScore score[] = new EdgeScore [scoreVec.size()];
    scoreVec.copyInto(score);
    Arrays.sort(score);

    // Mark which edges to hide.

    boolean hasHiddenEdge[] = new boolean [f.length];
    for (int i = 0; i < score.length; i++)
      {
        Edge ed = e[score[i].edge];
        if (hasHiddenEdge[ed.f1] || hasHiddenEdge[ed.f2])
          continue;
        hideEdge[score[i].edge] = true;
        hasHiddenEdge[ed.f1] = hasHiddenEdge[ed.f2] = true;
      }
  }
}
