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

/** The SceneViewer class is a component which displays a view of a Scene. */

public class SceneViewer extends ViewerCanvas
{
  Scene theScene;
  EditingWindow parentFrame;
  Vector cameras;
  boolean draggingBox, draggingSelectionBox, squareBox, sentClick, dragging;
  Point clickPoint, dragPoint;
  ObjectInfo clickedObject;
  int deselect;
  Renderer liveRenderer;
  LiveRenderOptions liveRenderOptions;
  SceneSubset liveRenderSubset;
  
  public class ViewRenderListener implements RenderListener
  {
    public void imageUpdated(Image image)
    {}

    public void statusChanged(String status)
    {}

    public void imageComplete(ComplexImage image)
    {
      updateWithLiveRenderImage(image.getImage());
      repaint();
    }

    public void renderingCanceled()
    {}
  }
  

  public SceneViewer(Scene s, RowContainer p, EditingWindow fr) //, LiveRenderOptions lro)  //(MB) RowContainer -> Container?
  {
    theScene = s;
    theScene.getModelEvent().addListener(this);
    parentFrame = fr;
    addEventLink(MouseClickedEvent.class, this, "mouseClicked");
    draggingBox = draggingSelectionBox = false;
    cameras = new Vector();
    buildChoices(p);
    rebuildCameraList();
    setRenderMode(ModellingApp.getPreferences().getDefaultDisplayMode());
    
    liveRenderer = null;
    liveRenderOptions = new LiveRenderOptions();
    liveRenderSubset = new SceneSubset(s);
  }
  
  
  // TODO(MB) Rewrite
  public LiveRenderOptions getLiveRenderOptions()
  {
    return liveRenderOptions;  //new LiveRenderOptions();
  }
  
  /** Activate/Deactivate live rendering functionality. */
  
  public void setLiveRendering(boolean active)
  {
    if (active)
      liveRenderer = getLiveRenderOptions().createRenderer();
    else
      liveRenderer = null;
  }
      
  /** Return true iff live rendering is active. */
      
  public boolean isLiveRendering()
  {
    return liveRenderer != null;
  }
  
  /** Returns the scene subset which should be transferred to the live renderer.
  */
  public SceneSubset getLiveRenderSubset()
  {
    return liveRenderSubset;
  }

  // TODO(MB) Make this called by containing windows
  public void dispose()
  {
    super.dispose();
    theScene.getModelEvent().removeListener(this);
  }


  /* Add all SceneCameras in the scene to list of available views. */

  public void rebuildCameraList()
  {
    int i = viewChoice.getItemCount()-2, selected = viewChoice.getSelectedIndex();

    while (i > 5)
      viewChoice.remove(i--);
    cameras.removeAllElements();
    for (i = 0; i < theScene.getNumObjects(); i++)
    {
      ObjectInfo obj = theScene.getObject(i);
      if (obj.object instanceof SceneCamera)
      {
        cameras.addElement(obj);
        viewChoice.add(viewChoice.getItemCount()-1, obj.name);
        if (obj == boundCamera)
          selected = viewChoice.getItemCount()-2;
      }
    }
    if (selected < viewChoice.getItemCount())
      viewChoice.setSelectedIndex(selected);
    else
      viewChoice.setSelectedIndex(viewChoice.getItemCount()-1);
    if (viewChoice.getParent() != null)
      viewChoice.getParent().layoutChildren();
  }

  /* Deal with selecting a SceneCamera from the choice menu. */

  public void selectOrientation(int which)
  {
    super.selectOrientation(which);
    if (which > 5 && which < viewChoice.getItemCount()-1)
    {
      boundCamera = (ObjectInfo) cameras.elementAt(which-6);
      CoordinateSystem coords = theCamera.getCameraCoordinates();
      coords.copyCoords(boundCamera.coords);
      theCamera.setCameraCoordinates(coords);
    }
    else
      boundCamera = null;
  }

  public int getOrientationChoice()
  {
    return viewChoice.getSelectedIndex();
  }

  public synchronized void updateImage()
  {
    updateWithLiveRenderImage(null);
    if (liveRenderer != null)
    {
      Renderer oldlr = liveRenderer;
      liveRenderer = oldlr.duplicate();
      oldlr.cancelRenderingAsync(getLiveRenderSubset());   // theScene);  // Async
      liveRenderer.renderScene(getLiveRenderSubset(), theCamera,
          this.new ViewRenderListener(), null);
    }
  }
  
  /** Update the viewer image using the liveRenderImage if not null. */
  public synchronized void updateWithLiveRenderImage(Image liveRenderImage)
  {
    Rectangle dim = getBounds();
    boolean render = (renderMode != RENDER_WIREFRAME);
    ObjectInfo obj;
    Vec3 viewdir;
    int i;

    adjustCamera(perspectiveChoice.getSelectedIndex() == 0);
    super.updateImage();
    if (theImage == null)
      return;
    
    try
    {
      if (liveRenderImage != null)
      {
        if (renderMode == RENDER_WIREFRAME)
          gr.drawImage(liveRenderImage, 0, 0, null);
        else
          {
            int width = liveRenderImage.getWidth(null);
            int height = liveRenderImage.getHeight(null);
            int maxi = (height < dim.height ? height : dim.height);
            int maxj = (width < dim.width ? width : dim.width);
            int frombase = 0, tobase = 0;

            java.awt.image.PixelGrabber pg = new java.awt.image.PixelGrabber(liveRenderImage, 0, 0, -1, -1, true);
            pg.grabPixels();
            for (i = 0; i < maxi; i++)
              System.arraycopy((int []) pg.getPixels(), width*i, pixel, dim.width*i, maxj);
          }
      }
    }
    catch(InterruptedException ie)
    {}

    if (liveRenderImage == null)  // TODO(MB) Remove this with rendering a subset of the objects
    {
      // Draw the objects.

      if (!render)
    {
      gr.setColor(Color.black);
      for (i = 0; i < theScene.getNumObjects(); i++)
      {
        obj = theScene.getObject(i);
        if (obj == boundCamera)
          continue;
        theCamera.setObjectTransform(obj.coords.fromLocal());
        drawObject(obj);
      }
    }
      else
    {
      viewdir = theCamera.getViewToWorld().timesDirection(Vec3.vz());
      for (i = 0; i < theScene.getNumObjects(); i++)
      {
        obj = theScene.getObject(i);
        if (obj == boundCamera)
          continue;
        theCamera.setObjectTransform(obj.coords.fromLocal());
        renderObject(obj, viewdir);
      }
    }
    }

    // Hilight the selection.

    if (currentTool.hilightSelection())
    {
      Rectangle bounds;
      int hsize;
      Color col;
      
      for (i = 0; i < theScene.getNumObjects(); i++)
      {
        obj = theScene.getObject(i);
        if (obj.selected)
        {
          hsize = Scene.HANDLE_SIZE;
          col = Color.red;
        }
        else if (obj.parentSelected)
        {
          hsize = Scene.HANDLE_SIZE/2;
          col = Color.magenta;
        }
        else
          continue;
        theCamera.setObjectTransform(obj.coords.fromLocal());
        bounds = theCamera.findScreenBounds(obj.getBounds());
        if (bounds != null)
        {
          drawBox(bounds.x, bounds.y, hsize, hsize, Color.red);
          drawBox(bounds.x+bounds.width-hsize+1, bounds.y, hsize, hsize, col);
          drawBox(bounds.x, bounds.y+bounds.height-hsize+1, hsize, hsize, col);
          drawBox(bounds.x+bounds.width-hsize+1, bounds.y+bounds.height-hsize+1, hsize, hsize, col);
          drawBox(bounds.x+(bounds.width-hsize)/2, bounds.y, hsize, hsize, col);
          drawBox(bounds.x, bounds.y+(bounds.height-hsize)/2, hsize, hsize, col);
          drawBox(bounds.x+(bounds.width-hsize)/2, bounds.y+bounds.height-hsize+1, hsize, hsize, col);
          drawBox(bounds.x+bounds.width-hsize+1, bounds.y+(bounds.height-hsize)/2, hsize, hsize, col);
        }
      }
    }
    
    // Finish up.
    
    drawBorder();
    if (showAxes)
      drawCoordinateAxes();
  }

  /** Draw a wireframe representation of a single object in the scene. */

  private void drawObject(ObjectInfo obj)
  {
    if (!obj.visible)
      return;
    if (obj.object instanceof ObjectCollection)
    {
      Mat4 m = theCamera.getObjectToWorld();
      Enumeration infoEnum = ((ObjectCollection) obj.object).getObjects(obj, true, theScene);
      while (infoEnum.hasMoreElements())
      {
        ObjectInfo info = (ObjectInfo) infoEnum.nextElement();
        CoordinateSystem coords = info.coords.duplicate();
        coords.transformCoordinates(m);
        theCamera.setObjectTransform(coords.fromLocal());
        drawObject(info);
      }
      return;
    }
    Object3D.draw(gr, theCamera, obj.getWireframePreview(), obj.getBounds());
  }

  /* Render a single object into the scene.  viewdir is the
     direction from which the object is being viewed in world coordinates. */

  private void renderObject(ObjectInfo obj, Vec3 viewdir)
  {
    RenderingMesh mesh;
    TextureSpec spec;
    int i;

    if (!obj.visible)
      return;
    if (theCamera.visibility(obj.getBounds()) == Camera.NOT_VISIBLE)
      return;
    if (obj.object instanceof ObjectCollection)
    {
      Mat4 m = theCamera.getObjectToWorld();
      Enumeration infoEnum = ((ObjectCollection) obj.object).getObjects(obj, true, theScene);
      while (infoEnum.hasMoreElements())
      {
        ObjectInfo info = (ObjectInfo) infoEnum.nextElement();
        CoordinateSystem coords = info.coords.duplicate();
        coords.transformCoordinates(m);
        theCamera.setObjectTransform(coords.fromLocal());
        renderObject(info, info.coords.toLocal().timesDirection(viewdir));
      }
      return;
    }
    mesh = obj.getPreviewMesh();
    if (mesh != null)
    {
      VertexShader shader;
      if (renderMode == RENDER_FLAT)
        shader = new FlatVertexShader(mesh, obj.object, theScene.getTime(), obj.coords.toLocal().timesDirection(viewdir));
      else if (renderMode == RENDER_SMOOTH)
        shader = new SmoothVertexShader(mesh, obj.object, theScene.getTime(), obj.coords.toLocal().timesDirection(viewdir));
      else
        shader = new TexturedVertexShader(mesh, obj.object, theScene.getTime(), obj.coords.toLocal().timesDirection(viewdir)).optimize();
      renderMesh(mesh, shader, theCamera, obj.object.isClosed(), null);
    }
    else
      renderWireframe(obj.getWireframePreview(), theCamera);
  }

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

  public void beginDraggingBox(Point p, boolean square)
  {
    draggingBox = true;
    clickPoint = p;
    squareBox = square;
    dragPoint = null;
  }

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

  protected void mousePressed(WidgetMouseEvent e)
  {
    int i, j, k, sel[], area, minarea;
    Rectangle bounds = null;
    ObjectInfo info;
    Point p;

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

    // 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)
    {
      moveToGrid(e);
      activeTool.mousePressed(e, this);
      return;
    }

    // See whether the click was on a currently selected object.

    p = e.getPoint();
    sel = theScene.getSelection();
    for (i = 0; i < sel.length; i++)
    {
      info = theScene.getObject(sel[i]);
      theCamera.setObjectTransform(info.coords.fromLocal());
      bounds = theCamera.findScreenBounds(info.getBounds());
      if (bounds != null && bounds.x <= p.x && bounds.y <= p.y && bounds.x+bounds.width >= p.x && bounds.y+bounds.height >= p.y)
      {
        clickedObject = info;
        break;
        }
    }
    if (i < sel.length)
    {
      // The click was on a selected object.  If it was a shift-click, the user may want
      // to deselect it, so set a flag.
      
      if (e.isShiftDown())
        deselect = sel[i];
      
      // If the current tool wants handle clicks, then check to see whether the click
      // was on a handle.
      
      if ((activeTool.whichClicks() & EditingTool.HANDLE_CLICKS) != 0)
      {
        if (p.x <= bounds.x+Scene.HANDLE_SIZE)
          j = 0;
        else if (p.x >= bounds.x+(bounds.width-Scene.HANDLE_SIZE)/2 && 
            p.x <= bounds.x+(bounds.width-Scene.HANDLE_SIZE)/2+Scene.HANDLE_SIZE)
          j = 1;
        else if (p.x >= bounds.x+bounds.width-Scene.HANDLE_SIZE)
          j = 2;
        else j = -1;
        if (p.y <= bounds.y+Scene.HANDLE_SIZE)
          k = 0;
        else if (p.y >= bounds.y+(bounds.height-Scene.HANDLE_SIZE)/2 && 
            p.y <= bounds.y+(bounds.height-Scene.HANDLE_SIZE)/2+Scene.HANDLE_SIZE)
          k = 1;
        else if (p.y >= bounds.y+bounds.height-Scene.HANDLE_SIZE)
          k = 2;
        else k = -1;
        if (k == 0)
        {
          moveToGrid(e);
          activeTool.mousePressedOnHandle(e, this, sel[i], j);
          return;
        }
        if (j == 0 && k == 1)
        {
          moveToGrid(e);
          activeTool.mousePressedOnHandle(e, this, sel[i], 3);
          return;
        }
        if (j == 2 && k == 1)
        {
          moveToGrid(e);
          activeTool.mousePressedOnHandle(e, this, sel[i], 4);
          return;
        }
        if (k == 2)
        {
          moveToGrid(e);
          activeTool.mousePressedOnHandle(e, this, sel[i], j+5);
          return;
        }
      }
      moveToGrid(e);
      dragging = false;
      if ((activeTool.whichClicks() & EditingTool.OBJECT_CLICKS) != 0)
        activeTool.mousePressedOnObject(e, this, sel[i]);
      else
        sentClick = false;
      return;
    }

    // The click was not on a selected object.  See whether it was on an unselected object.
    // If so, select it.  If appropriate, send an event to the current tool.

    // If the click was on top of multiple objects, the conventional thing to do is to select
    // the closest one.  I'm trying something different: select the smallest one.  This
    // should make it easier to select small objects which are surrounded by larger objects.
    // I may decide to change this, but it seemed like a good idea at the time...

    j = -1;
    minarea = Integer.MAX_VALUE;
    for (i = 0; i < theScene.getNumObjects(); i++)
    {
      info = theScene.getObject(i);
      if (info.visible)
      {
        theCamera.setObjectTransform(info.coords.fromLocal());
        bounds = theCamera.findScreenBounds(info.getBounds());
        if (bounds != null && bounds.contains(p))
          if (bounds.width*bounds.height < minarea)
          {
            j = i;
            minarea = bounds.width*bounds.height;
          }
      }
    }
    if (j > -1)
    {
      info = theScene.getObject(j);
      if (!e.isShiftDown())
      {
        if (parentFrame instanceof LayoutWindow)
          ((LayoutWindow) parentFrame).clearSelection();
        else
          theScene.clearSelection();
      }
      if (parentFrame instanceof LayoutWindow)
        ((LayoutWindow) parentFrame).addToSelection(j);
      else
        theScene.addToSelection(j);
      parentFrame.updateMenus();
      parentFrame.updateImage();
      moveToGrid(e);
      if ((activeTool.whichClicks() & EditingTool.OBJECT_CLICKS) != 0 && !e.isShiftDown())
        activeTool.mousePressedOnObject(e, this, j);
      else
        sentClick = false;
      clickedObject = info;
      return;
    }

    // The click was not on any object.  Start dragging a selection box.

    moveToGrid(e);
    draggingSelectionBox = true;
    beginDraggingBox(p, false);
    sentClick = false;
  }

  protected void mouseDragged(WidgetMouseEvent e)
  {
    moveToGrid(e);
    if (!dragging)
    {
      Point p = e.getPoint();
        if (clickPoint == null)   // TODO(MB) Remove hack (needed for live rendering)
          clickPoint = p;
      if (Math.abs(p.x-clickPoint.x) < 2 && Math.abs(p.y-clickPoint.y) < 2)
        return;
    }
    dragging = true;
    deselect = -1;
    Graphics g = getComponent().getGraphics();
    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);
  }

  protected void mouseReleased(WidgetMouseEvent e)
  {
    Graphics g = getComponent().getGraphics();
    Rectangle r, b;
    int i, j, sel[] = theScene.getSelection();
    ObjectInfo info;

    moveToGrid(e);

    // If the user was dragging a box, then erase it.

    if (draggingBox && dragPoint != null)
    {
      g.setXORMode(Color.white);
      g.setColor(Color.black);
      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));
    }

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

    if (draggingSelectionBox)
    {
      dragPoint = e.getPoint();
      r = 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));
      if (!e.isShiftDown())
      {
        if (parentFrame instanceof LayoutWindow)
          ((LayoutWindow) parentFrame).clearSelection();
        else
          theScene.clearSelection();
        parentFrame.updateMenus();
      }
      for (i = 0; i < theScene.getNumObjects(); i++)
      {
        info = theScene.getObject(i);
        if (info.visible)
        {
          theCamera.setObjectTransform(info.coords.fromLocal());
          b = theCamera.findScreenBounds(info.getBounds());
          if (b != null && b.intersects(r))
          {
            if (!e.isShiftDown())
            {
              if (parentFrame instanceof LayoutWindow)
                ((LayoutWindow) parentFrame).addToSelection(i);
              else
                theScene.addToSelection(i);
              parentFrame.updateMenus();
            }
            else
            {
              for (j = 0; j < sel.length && sel[j] != i; j++);
              if (j == sel.length)
              {
                if (parentFrame instanceof LayoutWindow)
                  ((LayoutWindow) parentFrame).addToSelection(i);
                else
                  theScene.addToSelection(i);
                parentFrame.updateMenus();
              }
            }
          }
        }
      }
      if (currentTool.hilightSelection())
        parentFrame.updateImage();
    }
    g.dispose();
    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 object and released the mouse without dragging,
    // then deselect the point.

    if (deselect > -1)
    {
      info = theScene.getObject(deselect);
      if (parentFrame instanceof LayoutWindow)
        ((LayoutWindow) parentFrame).removeFromSelection(deselect);
      else
        theScene.removeFromSelection(deselect);
      parentFrame.updateMenus();
      parentFrame.updateImage();
    }
  }
  
  /** Double-clicking on object should bring up its editor. */
  
  public void mouseClicked(MouseClickedEvent e)
  {
    if (e.getClickCount() == 2 && activeTool.whichClicks() != EditingTool.ALL_CLICKS && clickedObject != null && clickedObject.object.isEditable())
    {
      final Object3D obj = clickedObject.object;
      parentFrame.setUndoRecord(new UndoRecord(parentFrame, false, UndoRecord.COPY_OBJECT, new Object [] {obj, obj.duplicate()}));
      obj.edit(parentFrame, clickedObject,  new Runnable() {
	  public void run()
	  {
	    theScene.objectModified(obj);
	    parentFrame.updateImage();
	    parentFrame.updateMenus();
	  }
	} );
    }
  }

  /** Turn the grid on or off when the perspective changes. */
  
  protected void choiceChanged(WidgetEvent ev)
  {
    if (snapToGrid)
      theCamera.setGrid(gridSpacing/gridSubdivisions);
    else
      theCamera.setGrid(0.0);
    super.choiceChanged(ev);
  }

  protected boolean handleModelEvent(ModelEvent event)
  {
    if (!super.handleModelEvent(event))
    {
      if (event.getSource() == theScene)
      {
	updateImage();
	repaint();
      }
      else
        return false;
    }
    return true;
  }
}