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

/** MaterialPreviewer is a component used for renderering previews of Materials.  It displays
    a scene consisting of a Sphere with the desired Material applied to it, a ground plane,
    and a single light.  Optionally, an Object3D may be specified which will then be used
    instead of a Sphere. */

public class MaterialPreviewer extends CustomWidget implements RenderListener
{
  Scene theScene;
  Camera theCamera;
  Object3D theObject;
  ObjectInfo info;
  CoordinateSystem objectCoords;
  Image theImage;
  boolean mouseInside, renderInProgress;
  Point clickPoint;
  Mat4 rotMatrix;

  public static final int HANDLE_SIZE = 5;
  static final double DRAG_SCALE = Math.PI/360.0;

  /** Create a MaterialPreviewer to display a Texture and/or Material mapped to a sphere.
      Either tex or mat may be null. */

  public MaterialPreviewer(Texture tex, Material mat, int width, int height)
  {
    this(tex, mat, new Sphere(1.0, 1.0, 1.0), width, height);
  }
  
  /** Same as above, except you can specify a different object to use instead of a sphere. */

  public MaterialPreviewer(Texture tex, Material mat, Object3D obj, int width, int height)
  {
    ObjectInfo objInfo = new ObjectInfo(obj, new CoordinateSystem(), "");
    if (tex == null)
      tex = UniformTexture.invisibleTexture();
    objInfo.setTexture(tex, tex.getDefaultMapping());
    if (mat != null)
      objInfo.setMaterial(mat, mat.getDefaultMapping());
    init(objInfo, width, height);
  }

  /** Create a MaterialPreviewer to display the specified object, with its current texture
      and material. */

  public MaterialPreviewer(ObjectInfo obj, int width, int height)
  {
    init(obj.duplicate(), width, height);
  }
  
  /** Initialize the MaterialPreviewer. */
  
  private void init(ObjectInfo obj, int width, int height)
  {
    BoundingBox bounds = obj.getBounds();
    Vec3 size = bounds.getSize();
    double max = Math.max(size.x, Math.max(size.y, size.z))/2.0;
    double floor = -bounds.getSize().length()/2.0;
    CoordinateSystem coords = new CoordinateSystem(new Vec3(0.0, 0.0, 10.0*max), new Vec3(0.0, 0.0, -1.0), Vec3.vy());
    if (max > 10.0)
      max = 10.0;
    Vec3 vert[] = new Vec3 [] {new Vec3(100.0*max, floor, 100.0*max), new Vec3(-100.0*max, floor, 100.0*max), new Vec3(0.0, floor, -100.0*max)};
    int face[][] = {{0, 1, 2}};
    TriangleMesh tri;

    theScene = new Scene();
    theCamera = new Camera();
    theObject = obj.object;
    theCamera.setCameraCoordinates(coords);
    coords = new CoordinateSystem(new Vec3(), new Vec3(-0.5, -0.4, -1.0), Vec3.vy());
    theScene.addObject(new DirectionalLight(new RGBColor(1.0f, 1.0f, 1.0f), 0.8f), coords, "", null);
    coords = new CoordinateSystem(new Vec3(), Vec3.vz(), Vec3.vy());
    theScene.addObject(tri = new TriangleMesh(vert, face), coords, "", null);
    Texture tex = theScene.getDefaultTexture();
    tri.setTexture(tex, tex.getDefaultMapping());
    info = obj;
    objectCoords = info.coords = new CoordinateSystem();
    theScene.addObject(info, null);
    setPreferredSize(new Dimension(width, height));
    addEventLink(MousePressedEvent.class, this, "mousePressed");
    addEventLink(MouseReleasedEvent.class, this, "mouseReleased");
    addEventLink(MouseClickedEvent.class, this, "mouseClicked");
    addEventLink(MouseEnteredEvent.class, this, "mouseEntered");
    addEventLink(MouseExitedEvent.class, this, "mouseExited");
    addEventLink(MouseDraggedEvent.class, this, "mouseDragged");
    addEventLink(RepaintEvent.class, this, "paint");
    getComponent().addComponentListener(new ComponentAdapter() {
      public void componentResized(ComponentEvent ev)
      {
        render();
      }
    });
    getComponent().addHierarchyListener(new HierarchyListener() {
      public void hierarchyChanged(HierarchyEvent ev)
      {
        if ((ev.getChangeFlags()&HierarchyEvent.DISPLAYABILITY_CHANGED) != 0)
          if (!getComponent().isDisplayable())
          {
            Renderer rend = ModellingApp.getPreferences().getTexturePreviewRenderer();
            if (rend != null)
              rend.cancelRendering(theScene);
          }
      }
    });
    render();
  }
  
  /** Get the object on which the texture and material are being displayed. */
  
  public ObjectInfo getObject()
  {
    return info;
  }
  
  /* The following methods are used to modify the properties of the object being displayed. */

  public void setTexture(Texture tex, TextureMapping map)
  {
    if (tex == null)
      tex = UniformTexture.invisibleTexture();
    if (map == null)
      map = tex.getDefaultMapping();
    info.setTexture(tex, map);
  }
  
  public void setMaterial(Material mat, MaterialMapping map)
  {
    theObject.setMaterial(mat, map);
  }

  /** Render the preview. */

  public synchronized void render()
  {
    Renderer rend = ModellingApp.getPreferences().getTexturePreviewRenderer();
    if (rend == null)
      return;
    rend.cancelRendering(theScene);
    Rectangle bounds = getBounds();
    if (bounds.width == 0 || bounds.height == 0)
      return;
    theCamera.setSize(bounds.width, bounds.height);
    theCamera.setDistToScreen((bounds.height/200.0)/Math.tan(0.14));
    rend.configurePreview();
    System.out.println("MaterialPreviewer.render1");
    rend.renderScene(theScene, theCamera, this, null);
    renderInProgress = true;
    repaint();
  }

  /** Cancel rendering. */
  
  public synchronized void cancelRendering()
  {
    Renderer rend = ModellingApp.getPreferences().getTexturePreviewRenderer();
    if (rend != null)
      rend.cancelRendering(theScene);
  }

  private void paint(RepaintEvent ev)
  {
    Graphics2D g = ev.getGraphics();
    if (theImage != null)
      g.drawImage(theImage, 0, 0, getComponent());
    if (mouseInside)
      drawHilight(g);
    if (renderInProgress)
      {
        Rectangle bounds = getBounds();
        g.setColor(Color.red);
        g.drawRect(0, 0, bounds.width-1, bounds.height-1);
      }
  }

  private void drawHilight(Graphics g)
  {
    Rectangle bounds = getBounds();
    g.setColor(Color.red);
    g.fillRect(0, 0, HANDLE_SIZE, HANDLE_SIZE);
    g.fillRect(bounds.width-HANDLE_SIZE, 0, HANDLE_SIZE, HANDLE_SIZE);
    g.fillRect(0, bounds.height-HANDLE_SIZE, HANDLE_SIZE, HANDLE_SIZE);
    g.fillRect(bounds.width-HANDLE_SIZE, bounds.height-HANDLE_SIZE, HANDLE_SIZE, HANDLE_SIZE);
  }

  private void drawObject(Graphics g)
  {
    g.setColor(Color.gray);
    theCamera.setObjectTransform(rotMatrix.times(objectCoords.fromLocal()));
    Object3D.draw(g, theCamera, theObject.getWireframeMesh(), theObject.getBounds());
  }
/*
  public Dimension getMinimumSize()
  {
    return new Dimension(0, 0);
  }*/

  /** Called when more pixels are available for the current image. */
  
  public void imageUpdated(Image image)
  {
    theImage = image;
    repaint();
  }

  /** The renderer may call this method periodically during rendering, to give the listener text descriptions
      of the current status of rendering. */
  
  public void statusChanged(String status)
  {
  }

  /** Called when rendering is complete. */
  
  public void imageComplete(ComplexImage image)
  {
    theImage = image.getImage();
    renderInProgress = false;
    repaint();
  }
  
  /** Called when rendering is cancelled. */
  
  public void renderingCanceled()
  {
  }

  /* When the window containing this component is closed, cancel any rendering that is
     in progress. */
/*  
  private void addNotify()
  {
    super.addNotify();
    Component c = getParent();
    while (c != null && !(c instanceof Window))
      {
        c = c.getParent();
      }
    if (!(c instanceof Window))
      return;
    ((Window) c).addWindowListener(new WindowAdapter() {
      public void windowClosed(WindowEvent e)
      {
        Renderer rend = ModellingApp.getPreferences().getTexturePreviewRenderer();
        if (rend != null)
          rend.cancelRendering(theScene);
      } });
  }
*/
  private void mouseEntered(MouseEnteredEvent e)
  {
    mouseInside = true;
    Graphics g = getComponent().getGraphics();
    drawHilight(g);
    g.dispose();
  }
  
  private void mouseExited(MouseExitedEvent e)
  {
    mouseInside = false;
    repaint();
  }
  
  private void mousePressed(MousePressedEvent e)
  {
    Graphics g = getComponent().getGraphics();

    clickPoint = e.getPoint();
    Renderer rend = ModellingApp.getPreferences().getTexturePreviewRenderer();
    if (rend != null)
      rend.cancelRendering(theScene);
    rotMatrix = Mat4.identity();
    drawObject(g);
    g.dispose();
  }

  private void mouseReleased(MouseReleasedEvent e)
  {
    Point dragPoint = e.getPoint();
    double angle;
    Vec3 rotAxis;

    rotAxis = new Vec3((clickPoint.y-dragPoint.y)*DRAG_SCALE, (dragPoint.x-clickPoint.x)*DRAG_SCALE, 0.0);
    angle = rotAxis.length();
    if (angle != 0.0)
      {
	rotAxis = rotAxis.times(1.0/angle);
	rotAxis = theCamera.getViewToWorld().timesDirection(rotAxis);
	rotMatrix = Mat4.axisRotation(rotAxis, angle);
	objectCoords.transformAxes(rotMatrix);
      }
    render();
  }

  private void mouseClicked(MouseClickedEvent e)
  {
    if (e.getClickCount() == 2)
      {
	objectCoords.setOrientation(0.0, 0.0, 0.0);
	render();
      }
  }

  private void mouseDragged(MouseDraggedEvent e)
  {
    Graphics g = getComponent().getGraphics();
    Point dragPoint = e.getPoint();
    double angle;
    Vec3 rotAxis;

    rotAxis = new Vec3((clickPoint.y-dragPoint.y)*DRAG_SCALE, (dragPoint.x-clickPoint.x)*DRAG_SCALE, 0.0);
    angle = rotAxis.length();
    rotAxis = rotAxis.times(1.0/angle);
    rotAxis = theCamera.getViewToWorld().timesDirection(rotAxis);
    rotMatrix = Mat4.axisRotation(rotAxis, angle);
    g.drawImage(theImage, 0, 0, getComponent());
    drawHilight(g);
    drawObject(g);
    g.dispose();
  }
}
