/* 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.math.*;
import artofillusion.object.*;
import artofillusion.ui.*;
import buoy.event.*;
import buoy.widget.*;
import java.awt.*;
import java.util.*;

/** The CurveEditorWindow class represents the window for editing Curve objects. */

public class CurveEditorWindow extends MeshEditorWindow
{
  BMenu editMenu, meshMenu, smoothMenu;
  BMenuItem editMenuItem[], meshMenuItem[];
  BCheckBoxMenuItem smoothItem[];

  public CurveEditorWindow(EditingWindow parent, String title, ObjectInfo obj, Runnable onClose)
  {
    super(parent, title, obj, onClose);

    for(int i=0; i < theView.length; ++i)   // TODO(MB) Needed ?
      theView[i].setMeshVisible(true);
    
    MenuStructure ms = new MenuStructure();
    ms.loadStructure("Curve", false);
    menubar = ms.createMenuBar(this);
    setMenuBar(menubar);
    menuitemsCache = null;  // The menubar was rebuild so invalidate cache
    
    UIUtilities.recursivelyAddKeyPressedListeners(this);
    Dimension d1 = Toolkit.getDefaultToolkit().getScreenSize();
    Dimension d2 = new Dimension((d1.width*3)/4, (d1.height*3)/4);
    setBounds(new Rectangle((d1.width-d2.width)/2, (d1.height-d2.height)/2, d2.width, d2.height));
    updateMenus();
  }

  /** This constructor is here to let TubeEditorWindow subclass CurveEditorWindow. */

  protected CurveEditorWindow(EditingWindow parent, String title, ObjectInfo obj,
    Runnable onClose, int dummy)
  {
    super(parent, title, obj, onClose);
  }

//  void createEditMenu()
//  {
//    editMenu = Translate.menu("edit");
//    menubar.add(editMenu);
//    editMenuItem = new MenuItem [2];
//    editMenu.add( Translate.menuItem("undo", this));
//    editMenu.add(Translate.menuItem("selectAll", this));
//    editMenu.add(editMenuItem[0] = Translate.menuItem("extendSelection", this));
//    editMenu.add(editMenuItem[1] = Translate.checkboxMenuItem("freehandSelection", this, false));
//    editMenu.addSeparator();
//    editMenu.add(Translate.menuItem("curveTension", this));
//  }
//
//  void createMeshMenu(Curve obj)
//  {
//    MenuItem item;
//
//    meshMenu = Translate.menu("curve");
//    menubar.add(meshMenu);
//    meshMenuItem = new MenuItem [7];
//    meshMenu.add(meshMenuItem[0] = Translate.menuItem("deletePoints", this));
//    meshMenu.add(meshMenuItem[1] = Translate.menuItem("subdivide", this));
//    meshMenu.add(meshMenuItem[2] = Translate.menuItem("editPoints", this));
//    meshMenu.add(meshMenuItem[3] = Translate.menuItem("transformPoints", this));
//    meshMenu.add(meshMenuItem[4] = Translate.menuItem("randomize", this));
//    // Does not work: meshMenu.add(Translate.menuItem("centerCurve", this));
//    meshMenu.addSeparator();
//    meshMenu.add(meshMenuItem[5] = Translate.menuItem("smoothness", this));
//    meshMenu.add(smoothMenu = Translate.menu("smoothingMethod"));
//    smoothItem = new CheckboxMenuItem [3];
//    smoothMenu.add(smoothItem[0] = Translate.checkboxMenuItem("none", this, obj.getSmoothingMethod() == Curve.NO_SMOOTHING));
//    smoothMenu.add(smoothItem[1] = Translate.checkboxMenuItem("interpolating", this, obj.getSmoothingMethod() == Curve.INTERPOLATING));
//    smoothMenu.add(smoothItem[2] = Translate.checkboxMenuItem("approximating", this, obj.getSmoothingMethod() == Curve.APPROXIMATING));
//    meshMenu.add(meshMenuItem[6] = Translate.menuItem("closedEnds", this));
//    if (obj.isClosed())
//      meshMenuItem[6].setLabel(Translate.text("menu.openEnds"));
//  }
//
//  protected Menu createShowMenu()
//  {
//    Menu menu = Translate.menu("show");
//    menu.add(Translate.checkboxMenuItem("curve", this, true));
//    menu.add(Translate.checkboxMenuItem("entireScene", this,
//        getCurrentView().getSceneVisible()));
//    return menu;
//  }

  /** List of names of menu items which should be CheckboxMenuItem.
    Used by {@link #isMenuItemNameCheckbox()}.*/

  private static final String[] MENUITEMS_CHECKBOXITEMS=new String[]
    { "tolerantSelection", "freehandSelection", "none",
      "interpolating", "approximating", "detachSkeleton",
      "closedEnds",
      // Needed for tube:
      "openEnds", "flatEnds"
    };

    
  /** Tests if menu item named itemname should be a checkbox item. */
  protected boolean isMenuItemNameCheckbox(String itemname)
  {
    if (Utilities.findIndexEqual(MENUITEMS_CHECKBOXITEMS, itemname) > -1)
      return true;
    else
      return super.isMenuItemNameCheckbox(itemname);
  }
    
  /* EditingWindow methods. */

  public void setTool(EditingTool tool)
  {
    for(int i = 0; i < theView.length; ++i)
    {
      theView[i].setTool(tool);
    }
    currentTool = tool;
  }
  
  /** Returns true if the view has at least one selected
    element. */
  public boolean isSomethingSelected()
  {
    int i;
    boolean selected[] = ((CurveViewer) getCurrentView()).getSelection();

    for (i = 0; i < selected.length && !selected[i]; i++);

    return i < selected.length;
  }


  /** Updates a single menu item
    (setting enabled/disabled, (un)checked ...). */
  protected void updateMenuItem(MenuWidget item)
  {
    String itemname = ((Widget)item).getName();
    BCheckBoxMenuItem cbitem = null;
    BMenuItem mnitem = null;
    if (item instanceof BCheckBoxMenuItem)
      cbitem = (BCheckBoxMenuItem)item;
    else if (item instanceof BMenuItem)
      mnitem = (BMenuItem)item;
    
    Curve obj = (Curve) getCurrentView().getObject().object;

    // Edit Menu

    if ("extendSelection".equals(itemname))
      mnitem.setEnabled(isSomethingSelected());

    // Mesh Menu

    else if ("deletePoints".equals(itemname))
      mnitem.setEnabled(isSomethingSelected());
    else if ("subdivide".equals(itemname))
      mnitem.setEnabled(isSomethingSelected());
    else if ("editPoints".equals(itemname))
      mnitem.setEnabled(isSomethingSelected());
    else if ("transformPoints".equals(itemname))
      mnitem.setEnabled(isSomethingSelected());
    else if ("randomize".equals(itemname))
      mnitem.setEnabled(isSomethingSelected());
    else if ("centerCurve".equals(itemname))
      mnitem.setEnabled(isSomethingSelected());
    else if ("smoothness".equals(itemname))
      mnitem.setEnabled(isSomethingSelected());
    else if ("closedEnds".equals(itemname))
      cbitem.setState(obj.isClosed());
       
   // Smooth Menu in Mesh Menu

    else if ("none".equals(itemname))
      cbitem.setState(obj.getSmoothingMethod() == Curve.NO_SMOOTHING);
    else if ("interpolating".equals(itemname))
      cbitem.setState(obj.getSmoothingMethod() == Curve.INTERPOLATING);
    else if ("approximating".equals(itemname))
      cbitem.setState(obj.getSmoothingMethod() == Curve.APPROXIMATING);
    
    else super.updateMenuItem(item);
  }

  protected void keyPressed(KeyPressedEvent e)
  {
    int code = e.getKeyCode();

    if (code == KeyPressedEvent.VK_BACK_SPACE || code == KeyPressedEvent.VK_DELETE)
      deleteCommand();
    else
      super.keyPressed(e);
  }
  
  public void actionPerformed(CommandEvent e)
  {
    String command = e.getActionCommand();

    setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
    if (handleMenuAction(command)) // Try to call the command by reflection
      ;  // Do nothing
//    else if (command.equals("undo"))
//      undoCommand();
//    else if (command.equals("selectAll"))
//      selectAllCommand();
//    else if (command.equals("extendSelection"))
//      extendSelectionCommand();
    else if (command.equals("deletePoints"))
      deleteCommand();
//    else if (command.equals("subdivide"))
//      subdivideCommand();
    else if (command.equals("editPoints"))
      setPointsCommand();
//    else if (command.equals("transformPoints"))
//      transformPointsCommand();
//    else if (command.equals("randomize"))
//      randomizeCommand();
    else if (command.equals("centerCurve"))
      centerCommand();
    else if (command.equals("smoothness"))
      setSmoothnessCommand();
    else if (command.equals("curveTension"))
      setTensionCommand();
    else if (command.equals("grid"))
      setGridCommand();
    else if (command.equals("fourViews"))
      toggleViewsCommand();
    else if (command.equals("showTemplate"))
      {
        boolean wasShown = getCurrentView().getTemplateShown();
        for(int i = 0; i < theView.length; ++i)
          theView[i].setShowTemplate(!wasShown);
        updateMenus();
        updateImage();
      }
//    else if (command.equals("setTemplate"))
//      setTemplateCommand();
    setCursor(Cursor.getDefaultCursor());
  }

  public void itemStateChanged(CommandEvent e)
  {
    Object source = e.getSource();
    BCheckBoxMenuItem sourceitem = null;
    String itemname = "";

    if (source instanceof BCheckBoxMenuItem)
    {
      sourceitem = (BCheckBoxMenuItem)source;
      itemname = sourceitem.getName();
    }
    
    if ("freehandSelection".equals(itemname))
      {
	((CurveViewer) getCurrentView()).setFreehandSelection(sourceitem.getState());
	return;
      }
    if ("none".equals(itemname))
      setSmoothingMethod(Mesh.NO_SMOOTHING);
    else if ("interpolating".equals(itemname))
      setSmoothingMethod(Mesh.INTERPOLATING);
    else if ("approximating".equals(itemname))
      setSmoothingMethod(Mesh.APPROXIMATING);
    else if ("closedEnds".equals(itemname))
      toggleClosedCommand();

    super.itemStateChanged(e);
  }

  /** Creates a new view, containing the same Object3D with a
   * new VertexSelection.
   */
  protected MeshEditorWindow createNewViewNewSelection()
  {
    CurveEditorWindow w = new CurveEditorWindow(parentWindow, getTitle(), objInConstructor, onClose);
    // w.theView.setObject(theView.getObject().getObject3D());   TODO(MB) ??? Something with editGesture ???
    return w;
  }

  /** Create a MeshViewer which is appropriate for a particular editor window.
    Here a CurveViewer.*/
  
  public MeshViewer createMeshViewer(ObjectInfo obj, RowContainer p)
  {
    return new CurveViewer(obj,  p);
  }

  public void initBaseSelectionHolder(ObjectInfo obj)
  {
    baseSelHolder = new MeshSelectionHolder(obj);
    Curve mesh = (Curve) obj.object;
    baseSelHolder.setSelection(new boolean [mesh.getVertices().length]);
    baseSelHolder.setSelectionMode(0);
    baseSelHolder.decRefCount();  // Start with ref count 0.
    
    for (int i = 0; i < theView.length; ++i)
      ((CurveViewer) theView[i]).setSelectionHolder(baseSelHolder);
  }
  
//  public void initWindowMenus(ObjectInfo obj)
//  {  
//    menubar = new MenuBar();
//    setMenuBar(menubar);
//    createEditMenu();
//    createMeshMenu((Curve) obj.object);
//    createViewMenu();
//  }

  
  
  /** Create the toolbar panel on the left of an object editor. */
  
  public BorderContainer createToolbarPanel()
  {
    BorderContainer p4 = new BorderContainer();

    p4.add(tools = new ToolPalette(1, 7), BorderContainer.NORTH);
    tools.addTool(defaultTool = new ReshapeMeshTool(this));
    tools.addTool(new ScaleMeshTool(this));
    tools.addTool(new RotateMeshTool(this, false));
    tools.addTool(new SkewMeshTool(this));
    tools.addTool(new TaperMeshTool(this));
    tools.addTool(getMetaTool() /* MoveViewTool */);
    tools.addTool(getAltTool() /* RotateViewTool */);
    tools.selectTool(defaultTool);
 
    return p4;
  }
  
  
  
  public void selectAllCommand()
  {
    boolean selected[] = ((CurveViewer) getCurrentView()).getSelection();

    for (int i = 0; i < selected.length; i++)
      selected[i] = true;
    ((CurveViewer) getCurrentView()).setSelection(selected);
    ((CurveViewer) getCurrentView()).informSelectionChanged(this,
        ModelEvent.CHANGEDUR_OBJECTEDITOR);

  }

  /* Extend the selection outward by one edge. */

  public void extendSelectionCommand()
  {
    Curve theMesh = (Curve) getCurrentView().getObject().object;
    int oldDist = tensionDistance;
    tensionDistance = 1;
    int dist[] = ((CurveViewer) getCurrentView()).getSelectionDistance();
    boolean selected[] = new boolean [dist.length];
    tensionDistance = oldDist;

    for (int i = 0; i < dist.length; i++)
      selected[i] = (dist[i] == 0 || dist[i] == 1);
    ((CurveViewer) getCurrentView()).setSelection(selected);
    ((CurveViewer) getCurrentView()).informSelectionChanged(this,
        ModelEvent.CHANGEDUR_OBJECTEDITOR);
  }

  public void deleteCommand()
  {
    int i, j, num = 0;
    Curve theCurve = (Curve) getCurrentView().getObject().object;
    boolean selected[] = ((CurveViewer) getCurrentView()).getSelection(), newsel[];
    MeshVertex vt[] = theCurve.getVertices();
    float s[] = theCurve.getSmoothness(), news[];
    Vec3 v[];

    for (i = 0; i < selected.length; i++)
      if (selected[i])
	num++;
    if (num == 0)
      return;
    if (!theCurve.isClosed() && selected.length-num < 2)
      {
	new BStandardDialog("", Translate.text("curveNeeds2Points"), BStandardDialog.INFORMATION).showMessageDialog(this);
        return;
      }
    if (theCurve.isClosed() && selected.length-num < 3)
      {
	new BStandardDialog("", Translate.text("curveNeeds3Points"), BStandardDialog.INFORMATION).showMessageDialog(this);
	return;
      }
    setUndoRecord(new UndoRecord(this, false, UndoRecord.COPY_OBJECT, new Object [] {theCurve, theCurve.duplicate()}));
    v = new Vec3 [vt.length-num];
    news = new float [vt.length-num];
    newsel = new boolean [vt.length-num];
    for (i = 0, j = 0; i < vt.length; i++)
    {
      if (!selected[i])
      {
        newsel[j] = selected[i];
        news[j] = s[i];
        v[j++] = vt[i].r;
      }
    }
    theCurve.setShape(v, news);
    object3DChangedDuringEditor();
    ((CurveViewer) getCurrentView()).setSelection(newsel);
    ((CurveViewer) getCurrentView()).informSelectionChanged(this,
        ModelEvent.CHANGEDUR_OBJECTEDITOR);

  }

  public void subdivideCommand()
  {
    Curve theCurve = (Curve) getCurrentView().getObject().object;
    MeshVertex vt[] = theCurve.getVertices();
    float s[] = theCurve.getSmoothness(), news[];
    boolean selected[] = ((CurveViewer) getCurrentView()).getSelection(), newsel[], split[];
    Vec3 v[], newpos[];
    int i, j, p1, p3, p4, splitcount = 0, method = theCurve.getSmoothingMethod();

    v = new Vec3 [vt.length];
    for (i = 0; i < vt.length; i++)
      v[i] = vt[i].r;

    // Determine which parts need to be subdivided.

    if (theCurve.isClosed())
      split = new boolean [vt.length];
    else
      split = new boolean [vt.length-1];
    for (i = 0; i < split.length; i++)
      if (selected[i] && selected[(i+1)%selected.length])
      {
        split[i] = true;
        splitcount++;
      }
    newpos = new Vec3 [vt.length+splitcount];
    news = new float [vt.length+splitcount];
    newsel = new boolean [vt.length+splitcount];

    // Do the subdivision.

    for (i = 0, j = 0; i < split.length; i++)
    {
      newsel[j] = selected[i];
      p1 = i-1;
      if (p1 < 0)
      {
        if (theCurve.isClosed())
          p1 = v.length-1;
        else
          p1 = 0;
      }
      if (i < v.length-1)
        p3 = i+1;
      else
      {
        if (theCurve.isClosed())
          p3 = 0;
        else
          p3 = v.length-1;
      }
      if (selected[i] && method == Mesh.APPROXIMATING)
        newpos[j] = Curve.calcApproxPoint(v, s, p1, i, p3);
      else
        newpos[j] = vt[i].r;
      if (selected[i])
        news[j] = Math.min(s[i]*2.0f, 1.0f);
      else
        news[j] = s[i];
      if (!split[i])
      {
        j++;
        continue;
      }
      if (method == Mesh.NO_SMOOTHING)
        newpos[j+1] = v[i].plus(v[p3]).times(0.5);
      else if (method == Mesh.INTERPOLATING)
      {
        if (i < v.length-2)
          p4 = i+2;
        else
        {
          if (theCurve.isClosed())
            p4 = (i+2)%v.length;
          else
            p4 = v.length-1;
        }
        newpos[j+1] = Curve.calcInterpPoint(v, s, p1, i, p3, p4);
      }
      else
        newpos[j+1] = v[i].plus(v[p3]).times(0.5);
      news[j+1] = 1.0f;
      newsel[j+1] = true;
      j += 2;
    }
    if (!theCurve.isClosed())
    {
      newpos[0] = v[0];
      newpos[j] = v[i];
      news[j] = s[i];
      newsel[j] = selected[i];
    }
    setUndoRecord(new UndoRecord(this, false, UndoRecord.COPY_OBJECT, new Object [] {theCurve, theCurve.duplicate()}));
    theCurve.setShape(newpos, news);
    object3DChangedDuringEditor();
    ((CurveViewer) getCurrentView()).setSelection(newsel);
    ((CurveViewer) getCurrentView()).informSelectionChanged(this,
        ModelEvent.CHANGEDUR_OBJECTEDITOR);
  }

  public void setSmoothnessCommand()
  {
    final Curve theCurve = (Curve) getCurrentView().getObject().object;
    Curve oldCurve = (Curve) theCurve.duplicate();
    final MeshVertex vt[] = theCurve.getVertices();
    final boolean selected[] = ((CurveViewer) getCurrentView()).getSelection();
    final float s[] = theCurve.getSmoothness();
    float value;
    final ValueSlider smoothness;
    int i;

    for (i = 0; i < selected.length && !selected[i]; i++);
    if (i == selected.length)
      return;
    value = 0.001f * (Math.round(s[i]*1000.0f));
    smoothness = new ValueSlider(0.0, 1.0, 100, (double) value);
    smoothness.addEventLink(ValueChangedEvent.class, new Object() {
      void processEvent()
      {
        float sm = (float) smoothness.getValue();
        float news[] = new float [vt.length];
        for (int i = 0; i < selected.length; i++)
          news[i] = selected[i] ? sm : s[i];
        theCurve.setSmoothness(news);
        getCurrentView().objectChanged();  // TODO(MB) Use message mechanism
        getCurrentView().updateImage();
        getCurrentView().repaint();
      }
    } );
    ComponentsDialog dlg = new ComponentsDialog(this, Translate.text("setPointSmoothness"), new Widget [] {smoothness},
    	new String [] {Translate.text("Smoothness")});
    if (dlg.clickedOk())
    {
      setUndoRecord(new UndoRecord(this, false, UndoRecord.COPY_OBJECT, new Object [] {theCurve, oldCurve}));
      object3DChangedDuringEditor();
    }
    else
    {
      theCurve.copyObject(oldCurve);
      getCurrentView().objectChanged();
      getCurrentView().updateImage();
      getCurrentView().repaint();
    }
  }

  void setSmoothingMethod(int method)
  {
    Curve theCurve = (Curve) getCurrentView().getObject().object;

    setUndoRecord(new UndoRecord(this, false, UndoRecord.COPY_OBJECT, new Object [] {theCurve, theCurve.duplicate()}));
    theCurve.setSmoothingMethod(method);
    updateMenus();
    object3DChangedDuringEditor();
  }

  public void toggleClosedCommand()
  {
    Curve theCurve = (Curve) getCurrentView().getObject().object;

    setUndoRecord(new UndoRecord(this, false, UndoRecord.COPY_OBJECT, new Object [] {theCurve, theCurve.duplicate()}));
    if (theCurve.isClosed())
    {
      theCurve.setClosed(false);
      meshMenuItem[6].setText(Translate.text("menu.closedEnds"));
    }
    else
    {
      theCurve.setClosed(true);
      meshMenuItem[6].setText(Translate.text("menu.openEnds"));
    }
    object3DChangedDuringEditor();
  }

  /* Given a list of deltas which will be added to the selected vertices, calculate the
     corresponding deltas for the unselected vertices according to the mesh tension. */

  public void adjustDeltas(Vec3 delta[])
  {
    int dist[] = getCurrentView().getSelectionDistance(), count[] = new int [delta.length];
    Curve theCurve = (Curve) getCurrentView().getObject().object;
    int maxDistance = getTensionDistance();
    double tension = getMeshTension(), scale[] = new double [maxDistance+1];

    for (int i = 0; i < delta.length; i++)
      if (dist[i] != 0)
        delta[i].set(0.0, 0.0, 0.0);
    for (int i = 0; i < maxDistance; i++)
    {
      for (int j = 0; j < count.length; j++)
        count[j] = 0;
      for (int j = 0; j < dist.length-1; j++)
      {
        if (dist[j] == i && dist[j+1] == i+1)
        {
          count[j+1]++;
          delta[j+1].add(delta[j]);
        }
        else if (dist[j+1] == i && dist[j] == i+1)
        {
          count[j]++;
          delta[j].add(delta[j+1]);
        }
      }
      if (theCurve.isClosed())
      {
        if (dist[0] == i && dist[dist.length-1] == i+1)
        {
          count[dist.length-1]++;
          delta[dist.length-1].add(delta[0]);
        }
        else if (dist[dist.length-1] == i && dist[0] == i+1)
        {
          count[0]++;
          delta[0].add(delta[dist.length-1]);
        }
      }
      for (int j = 0; j < count.length; j++)
        if (count[j] > 1)
          delta[j].scale(1.0/count[j]);
    }
    for (int i = 0; i < scale.length; i++)
      scale[i] = Math.pow((maxDistance-i+1.0)/(maxDistance+1.0), tension);
    for (int i = 0; i < delta.length; i++)
      if (dist[i] > 0)
        delta[i].scale(scale[dist[i]]);
  }

}