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

/** The ObjectViewer class is the abstract superclass of components which display 
   a single object and allow the user to edit it. */

public abstract class ObjectViewer extends ViewerCanvas
{
  protected boolean showScene, useWorldCoords, freehandSelection, draggingBox, squareBox, sentClick;
  protected Point clickPoint, dragPoint;
  protected Vector selectBoundsPoints;
  protected Shape selectBounds;
  protected Object3D theObject;
  protected ObjectInfo objInfo, thisObjectInScene;
  protected Scene theScene;

  protected static int lastRenderMode = RENDER_TRANSPARENT;
  protected static boolean lastShowScene;
  protected static boolean lastUseWorldCoords;
  protected static boolean lastShowGrid;
  protected static boolean lastSnapToGrid;
  protected static boolean lastFreehand;
  protected static int lastGridSubdivisions = 10;
  protected static double lastGridSpacing = 1.0;

  static final RGBColor surfaceRGB = new RGBColor(0.8f, 0.8f, 1.0f);
  static final Color surfaceColor = new Color(0.8f, 0.8f, 1.0f);

  public ModelEvent modelevent=new ModelEvent();  // TODO(MB) Remove
  
  public ObjectViewer(ObjectInfo obj, RowContainer p)
  {
    if (obj != null)
    {
      theObject = obj.object;
        theObject.getModelEvent().addListener(this);
      objInfo = obj.duplicate();
      objInfo.coords.setOrigin(new Vec3());
      objInfo.coords.setOrientation(Vec3.vz(), Vec3.vy());
      objInfo.clearDistortion();
    }
    buildChoices(p);
    setRenderMode(lastRenderMode);
    setSceneVisible(lastShowScene);
    setUseWorldCoords(lastUseWorldCoords);
    setGrid(lastGridSpacing, lastGridSubdivisions, lastShowGrid, lastSnapToGrid);
    setFreehandSelection(lastFreehand);
  }

  public void dispose()
  {
    if (theObject != null)
      theObject.getModelEvent().removeListener(this);
  }

  public ObjectInfo getObject()
  {
    return objInfo;
  }

  public void setObject(Object3D obj)
  {
    theObject = obj;
    if (objInfo == null)
      objInfo = new ObjectInfo(obj, new CoordinateSystem(), "");
    else
      objInfo.object = obj;
    objInfo.clearCachedMeshes();
  }

  /** Refreshes internal data if the viewed object changed. */
  public void updateInternalState()
  {
    setObject(theObject);
  }

  /** Deprecated: This should be called whenever the object has changed. TODO(MB) Remove method.
   * @param cause The cause (caller) of the notification.
   */
  public void objectChanged(Object cause)
  {
    objInfo.clearCachedMeshes();
    modelevent.send(this, "object changed", "", cause);
  }

  /** Depr.: This should be called whenever the object has changed. TODO(MB) Remove method*/
  public void objectChanged()
  {
    objectChanged(null);
  }

  protected boolean handleModelEvent(ModelEvent event)
  {
    if (!super.handleModelEvent(event))
    {
      if (event.getSource() == theObject)
      {
        if (event.hasKey("object3d changed") && event.getValue("happened during") != ModelEvent.CHANGEDUR_OBJECTEDITORDRAG)
        {
          updateInternalState();
          updateImage();
          repaint();
        }
        else
          return false;
      }
      else
        return false;
    }
    return true;
  }

  public synchronized void updateImage()
  {
    Rectangle dim = getBounds();

    adjustCamera(perspectiveChoice.getSelectedIndex() == 0);
    super.updateImage();
    if (theImage == null || objInfo == null)
      return;

    // Draw the rest of the objects in the scene.

    if (gr != null)
      gr.setColor(Color.black);
    if (showScene && theScene != null)
    {
      for (int i = 0; i < theScene.getNumObjects(); i++)
      {
        ObjectInfo obj = theScene.getObject(i);
        drawSceneObject(obj, obj.coords);
      }
    }

    // Draw the object being edited.

    
    theCamera.setObjectTransform(getDisplayCoordinates().fromLocal());
    drawObject(gr);

    // Finish up.

    drawBorder();
    if (showAxes)
      drawCoordinateAxes();
  }

  /** Draw one object in the scene. */

  private void drawSceneObject(ObjectInfo obj, CoordinateSystem coords)
  {
    if (obj == thisObjectInScene || !obj.visible)
      return;
    if (obj.object instanceof ObjectCollection)
    {
      Enumeration enum = ((ObjectCollection) obj.object).getObjects(obj, true, theScene);
      while (enum.hasMoreElements())
      {
        ObjectInfo elem = (ObjectInfo) enum.nextElement();
        CoordinateSystem elemCoords = elem.coords.duplicate();
        elemCoords.transformCoordinates(coords.fromLocal());
        drawSceneObject(elem, elemCoords);
      }
      return;
    }
    if (useWorldCoords)
      theCamera.setObjectTransform(coords.fromLocal());
    else
      theCamera.setObjectTransform(thisObjectInScene.coords.toLocal().times(coords.fromLocal()));
    if (theCamera.visibility(obj.getBounds()) == Camera.NOT_VISIBLE)
      return;
    if (renderMode == RENDER_WIREFRAME)
      Object3D.draw(gr, theCamera, obj.getWireframePreview(), obj.getBounds());
    else
    {
      RenderingMesh mesh = obj.getPreviewMesh();
      if (mesh == null)
        renderWireframe(obj.getWireframePreview(), theCamera);
      else if (renderMode == RENDER_TRANSPARENT)
        renderMeshTransparent(mesh, theCamera, theCamera.getViewToWorld().timesDirection(Vec3.vz()), surfaceRGB, null);
      else
      {
        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, obj.object, 0.0, viewDir).optimize();
        renderMesh(mesh, shader, theCamera, obj.object.isClosed(), null);
      }
    }
  }

  protected abstract void drawObject(Graphics g);

  /** Get the coordinate system in which the object is displayed.  This will
      vary depending on whether the user has selected Local or Scene coordinates. */

  public CoordinateSystem getDisplayCoordinates()
  {
    if (useWorldCoords && thisObjectInScene != null)
      return thisObjectInScene.coords;
    else
      return objInfo.coords;
  }

  /** Keep track of the last render mode which the user chose, so we can go back to it
     when opening new windows. */

  public void setRenderMode(int mode)
  {
    super.setRenderMode(mode);
    lastRenderMode = mode;
  }

  /** Get whether freehand selection mode is currently in use. */

  public boolean getFreehandSelection()
  {
    return freehandSelection;
  }

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

  public void setFreehandSelection(boolean freehand)
  {
    freehandSelection = freehand;
    lastFreehand = freehand;
  }

  /** Get the scene this object is part of, or null if there is none. */

  public Scene getScene()
  {
    return theScene;
  }

  /** Set the scene this object is part of. */

  public void setScene(Scene sc, ObjectInfo thisObject)
  {
    theScene = sc;
    thisObjectInScene = thisObject;
  }

  /** Get whether the entire scene is visible. */

  public boolean getSceneVisible()
  {
    return showScene;
  }

  /** Set whether the entire scene is visible. */

  public void setSceneVisible(boolean visible)
  {
    lastShowScene = showScene = visible;
  }

  /** Get whether to use world coordinates. */

  public boolean getUseWorldCoords()
  {
    return useWorldCoords;
  }

  /** Set whether to use world coordinates. */

  public void setUseWorldCoords(boolean use)
  {
    lastUseWorldCoords = useWorldCoords = use;
  }

  /** Set the grid parameters for this view.  This is overridden to remember
      parameters between windows. */

  public void setGrid(double spacing, int subdivisions, boolean show, boolean snap)
  {
    super.setGrid(spacing, subdivisions, show, snap);
    lastGridSpacing = spacing;
    lastGridSubdivisions = subdivisions;
    lastShowGrid = show;
    lastSnapToGrid = snap;
  }

  /** Begin dragging a selection region.  The variable square determines whether
      the region should be constrained to be square. */

  public void beginDraggingSelection(Point p, boolean square)
  {
    draggingBox = true;
    clickPoint = p;
    squareBox = square;
    dragPoint = null;
    if (freehandSelection)
      selectBoundsPoints = new Vector();
  }

  /** Finish dragging a selection region. */

  public void endDraggingSelection()
  {
    if (!draggingBox || dragPoint == null)
    {
      selectBounds = null;
      return;
    }
    repaint();

    // Construct the selection region.

    if (freehandSelection)
    {
      int n = selectBoundsPoints.size(), x[] = new int [n], y[] = new int [n];
      for (int i = 0; i < n; i++)
      {
        Point p = (Point) selectBoundsPoints.elementAt(i);
        x[i] = p.x;
        y[i] = p.y;
      }
      selectBounds = new Polygon(x, y, n);
    }
    else
      selectBounds = new Rectangle(Math.min(clickPoint.x, dragPoint.x), Math.min(clickPoint.y, dragPoint.y),
		Math.abs(dragPoint.x-clickPoint.x), Math.abs(dragPoint.y-clickPoint.y));
  }

  /** Determine whether the selection region contains the specified point. */

  public boolean selectionRegionContains(Point p)
  {
    if (selectBounds instanceof Rectangle)
      return ((Rectangle) selectBounds).contains(p);
    if (selectBounds instanceof Polygon)
      return ((Polygon) selectBounds).contains(p);
    return false;
  }

  protected void mouseDragged(WidgetMouseEvent e)
  {
    Graphics g = getComponent().getGraphics();

    moveToGrid(e);
    if (draggingBox && freehandSelection)
    {
      // Add this point to the region boundary and draw a line.
      
      Point prev = (dragPoint == null ? clickPoint : dragPoint);
      dragPoint = e.getPoint();
      g.setColor(Color.black);
      g.drawLine(prev.x, prev.y, dragPoint.x, dragPoint.y);
      selectBoundsPoints.addElement(dragPoint);
    }
    else if (draggingBox)
    {
      // We are dragging a box, so erase and redraw it.

      g.setXORMode(Color.white);
      g.setColor(Color.black);
      if (dragPoint != null)
        g.drawRect(Math.min(clickPoint.x, dragPoint.x), Math.min(clickPoint.y, dragPoint.y), 
              Math.abs(dragPoint.x-clickPoint.x), Math.abs(dragPoint.y-clickPoint.y));
      dragPoint = e.getPoint();
      if (squareBox)
      {
        if (Math.abs(dragPoint.x-clickPoint.x) > Math.abs(dragPoint.y-clickPoint.y))
        {
          if (dragPoint.y < clickPoint.y)
            dragPoint.y = clickPoint.y - Math.abs(dragPoint.x-clickPoint.x);
          else
            dragPoint.y = clickPoint.y + Math.abs(dragPoint.x-clickPoint.x);
        }
        else
        {
          if (dragPoint.x < clickPoint.x)
            dragPoint.x = clickPoint.x - Math.abs(dragPoint.y-clickPoint.y);
          else
            dragPoint.x = clickPoint.x + Math.abs(dragPoint.y-clickPoint.y);
        }
      }
      g.drawRect(Math.min(clickPoint.x, dragPoint.x), Math.min(clickPoint.y, dragPoint.y), 
              Math.abs(dragPoint.x-clickPoint.x), Math.abs(dragPoint.y-clickPoint.y));
    }
    g.dispose();

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

    if (sentClick)
      activeTool.mouseDragged(e, this);
  }

  public void previewObject()
  {
    Scene sc = new Scene();
    double dist = theCamera.getDistToScreen();
    Renderer rend = ModellingApp.getPreferences().getObjectPreviewRenderer();

    if (rend == null)
      return;
    sc.addObject(new DirectionalLight(new RGBColor(1.0f, 1.0f, 1.0f), 0.8f), theCamera.getCameraCoordinates(), "", null);
    sc.addObject(objInfo, null);
    adjustCamera(true);
    rend.configurePreview();
    ObjectInfo cameraInfo = new ObjectInfo(new SceneCamera(), theCamera.getCameraCoordinates(), "");
    new RenderingDialog(UIUtilities.findFrame(this), rend, sc, theCamera, cameraInfo);
    adjustCamera(perspectiveChoice.getSelectedIndex() == 0);
  }
}