/*
 * ModelPreviewComponent 16 jan. 2010
 *
 * Sweet Home 3D, Copyright (c) 2007-2010 Emmanuel PUYBARET / eTeks <info@eteks.com>
 *
 * 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.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */
package com.eteks.sweethome3d.swing;

import java.awt.AWTException;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.GraphicsConfiguration;
import java.awt.GraphicsEnvironment;
import java.awt.GridLayout;
import java.awt.KeyboardFocusManager;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.Robot;
import java.awt.Toolkit;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.awt.event.MouseWheelEvent;
import java.awt.event.MouseWheelListener;
import java.awt.image.BufferedImage;
import java.awt.image.MemoryImageSource;
import java.io.File;
import java.io.IOException;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;

import javax.imageio.ImageIO;
import javax.media.j3d.AmbientLight;
import javax.media.j3d.Appearance;
import javax.media.j3d.Background;
import javax.media.j3d.BoundingSphere;
import javax.media.j3d.BranchGroup;
import javax.media.j3d.Canvas3D;
import javax.media.j3d.DirectionalLight;
import javax.media.j3d.GraphicsConfigTemplate3D;
import javax.media.j3d.Group;
import javax.media.j3d.Light;
import javax.media.j3d.Link;
import javax.media.j3d.Node;
import javax.media.j3d.PhysicalBody;
import javax.media.j3d.PhysicalEnvironment;
import javax.media.j3d.Shape3D;
import javax.media.j3d.Texture;
import javax.media.j3d.Transform3D;
import javax.media.j3d.TransformGroup;
import javax.media.j3d.View;
import javax.swing.BorderFactory;
import javax.swing.JComponent;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
import javax.swing.border.EtchedBorder;
import javax.swing.event.AncestorEvent;
import javax.swing.event.AncestorListener;
import javax.swing.event.MouseInputAdapter;
import javax.vecmath.Color3f;
import javax.vecmath.Matrix3f;
import javax.vecmath.Point3d;
import javax.vecmath.Vector3d;
import javax.vecmath.Vector3f;

import com.eteks.sweethome3d.j3d.Component3DManager;
import com.eteks.sweethome3d.j3d.HomePieceOfFurniture3D;
import com.eteks.sweethome3d.j3d.ModelManager;
import com.eteks.sweethome3d.model.CatalogPieceOfFurniture;
import com.eteks.sweethome3d.model.Content;
import com.eteks.sweethome3d.model.HomeMaterial;
import com.eteks.sweethome3d.model.HomePieceOfFurniture;
import com.eteks.sweethome3d.tools.OperatingSystem;
import com.eteks.sweethome3d.tools.TemporaryURLContent;
import com.sun.j3d.exp.swing.JCanvas3D;
import com.sun.j3d.utils.universe.SimpleUniverse;
import com.sun.j3d.utils.universe.Viewer;
import com.sun.j3d.utils.universe.ViewingPlatform;

/**
 * Super class of 3D preview component for model. 
 */
public class ModelPreviewComponent extends JComponent {
  private SimpleUniverse          universe;
  private JPanel                  component3DPanel;
  private Component               component3D;
  private BranchGroup             sceneTree;
  private float                   viewYaw   = (float) Math.PI / 8;
  private float                   viewPitch = -(float) Math.PI / 16;
  private float                   viewScale = 1;
  private Object                  iconImageLock;
  private HomePieceOfFurniture    previewedPiece;
  private Map<Texture, Texture>   pieceTextures = new HashMap<Texture, Texture>();

  /**
   * Returns an 3D model preview component.
   */
  public ModelPreviewComponent() {
    this(false);
  }
  
  /**
   * Returns an 3D model preview component that lets the user change its pitch and scale 
   * if <code>pitchAndScaleChangeSupported</code> is <code>true</code>.
   */
  public ModelPreviewComponent(boolean pitchAndScaleChangeSupported) {
    setBorder(BorderFactory.createEtchedBorder(EtchedBorder.LOWERED));

    this.sceneTree = createSceneTree();
    
    this.component3DPanel = new JPanel();
    setLayout(new BorderLayout());
    add(this.component3DPanel);

    GraphicsEnvironment graphicsEnvironment = GraphicsEnvironment.getLocalGraphicsEnvironment();
    if (graphicsEnvironment.getScreenDevices().length == 1) {
      // If only one screen device is available, create 3D component immediately, 
      // otherwise create it once the screen device of the parent is known
      createComponent3D(graphicsEnvironment.getDefaultScreenDevice().getDefaultConfiguration(), pitchAndScaleChangeSupported);
    }

    // Add an ancestor listener to create 3D component and its universe once this component is made visible 
    // and clean up universe once its parent frame is disposed
    addAncestorListener(pitchAndScaleChangeSupported);
  }

  /**
   * Returns component preferred size.
   */
  @Override
  public Dimension getPreferredSize() {
    if (isPreferredSizeSet()) {
      return super.getPreferredSize();
    } else {
      return new Dimension(200, 200);
    }
  }

  @Override
  public void addMouseMotionListener(final MouseMotionListener l) {
    super.addMouseMotionListener(l);
    if (this.component3D != null) {
      this.component3D.addMouseMotionListener(new MouseMotionListener() {
          public void mouseMoved(MouseEvent ev) {
            l.mouseMoved(SwingUtilities.convertMouseEvent(component3D, ev, ModelPreviewComponent.this));
          }
          
          public void mouseDragged(MouseEvent ev) {
            l.mouseDragged(SwingUtilities.convertMouseEvent(component3D, ev, ModelPreviewComponent.this));
          }
        });
    }
  }
  
  @Override
  public void addMouseListener(final MouseListener l) {
    super.addMouseListener(l);
    if (this.component3D != null) {
      this.component3D.addMouseListener(new MouseListener() {
          public void mouseReleased(MouseEvent ev) {
            l.mouseReleased(SwingUtilities.convertMouseEvent(component3D, ev, ModelPreviewComponent.this));
          }
          
          public void mousePressed(MouseEvent ev) {
            l.mousePressed(SwingUtilities.convertMouseEvent(component3D, ev, ModelPreviewComponent.this));
          }
          
          public void mouseExited(MouseEvent ev) {
            l.mouseExited(SwingUtilities.convertMouseEvent(component3D, ev, ModelPreviewComponent.this));
          }
          
          public void mouseEntered(MouseEvent ev) {
            l.mouseEntered(SwingUtilities.convertMouseEvent(component3D, ev, ModelPreviewComponent.this));
          }
          
          public void mouseClicked(MouseEvent ev) {
            l.mouseClicked(SwingUtilities.convertMouseEvent(component3D, ev, ModelPreviewComponent.this));
          }
        });
    }
  }
  
  /**
   * Returns the 3D component of this component.
   */
  JComponent getComponent3D() {
    return this.component3DPanel;
  }
  
  /**
   * Adds an ancestor listener to this component to manage the creation of the 3D component and its universe 
   * and clean up the universe.  
   */
  private void addAncestorListener(final boolean pitchAndScaleChangeSupported) {
    addAncestorListener(new AncestorListener() {
        public void ancestorAdded(AncestorEvent ev) {
          if (component3D == null) {
            createComponent3D(ev.getAncestor().getGraphicsConfiguration(), pitchAndScaleChangeSupported);
          }
          if (universe == null) {
            createUniverse();
          }
        }
        
        public void ancestorRemoved(AncestorEvent ev) {
          if (universe != null) {
            disposeUniverse();
          }
        }
        
        public void ancestorMoved(AncestorEvent ev) {
        }        
      });
  }
  
  /**
   * Creates the 3D component associated with the given <code>configuration</code> device.
   */
  private void createComponent3D(GraphicsConfiguration graphicsConfiguration, 
                              boolean pitchAndScaleChangeSupported) {
    if (Boolean.valueOf(System.getProperty("com.eteks.sweethome3d.j3d.useOffScreen3DView", "false"))) {
      GraphicsConfigTemplate3D gc = new GraphicsConfigTemplate3D();
      gc.setSceneAntialiasing(GraphicsConfigTemplate3D.PREFERRED);
      try {
        // Instantiate JCanvas3DWithNotifiedPaint inner class by reflection
        // to be able to run under Java 3D 1.3
        this.component3D = (Component)Class.forName(ModelPreviewComponent.class.getName() + "$JCanvas3DWithNotifiedPaint").
            getConstructor(ModelPreviewComponent.class, GraphicsConfigTemplate3D.class).newInstance(this, gc);
      } catch (ClassNotFoundException ex) {
        throw new UnsupportedOperationException("Java 3D 1.5 required to display an offscreen 3D view");
      } catch (Exception ex) {
        UnsupportedOperationException ex2 = new UnsupportedOperationException();
        ex2.initCause(ex);
        throw ex2;
      }
    } else {
      this.component3D = Component3DManager.getInstance().getOnscreenCanvas3D(graphicsConfiguration,
          new Component3DManager.RenderingObserver() {
            public void canvas3DPreRendered(Canvas3D canvas3d) {
            }
            
            public void canvas3DPostRendered(Canvas3D canvas3d) {
            }
            
            public void canvas3DSwapped(Canvas3D canvas3d) {
              ModelPreviewComponent.this.canvas3DSwapped();
            }            
          });
    }
    this.component3D.setBackground(new Color(0xE5E5E5));

    // Layout 3D component
    this.component3DPanel.setLayout(new GridLayout());
    this.component3DPanel.add(this.component3D);
    this.component3D.setFocusable(false);      
    addMouseListeners(this.component3D, pitchAndScaleChangeSupported);
  }

  /**
   * A <code>JCanvas</code> canvas that sends a notification when it's drawn.
   */
  private static class JCanvas3DWithNotifiedPaint extends JCanvas3D {
    private final ModelPreviewComponent homeComponent3D;

    public JCanvas3DWithNotifiedPaint(ModelPreviewComponent component,
                                      GraphicsConfigTemplate3D template) {
      super(template);
      this.homeComponent3D = component;
    }

    public void paintComponent(Graphics g) {
      super.paintComponent(g);
      this.homeComponent3D.canvas3DSwapped();
    }
  }
  
  /**
   * Adds an AWT mouse listener to component that will update view platform transform.  
   */
  private void addMouseListeners(final Component component3D, 
                                 final boolean pitchAndScaleChangeSupported) {
    final float ANGLE_FACTOR = 0.02f;
    final float ZOOM_FACTOR = 0.02f;
    MouseInputAdapter mouseListener = new MouseInputAdapter() {
        private int xLastMouseMove;
        private int yLastMouseMove;
        
        @Override
        public void mousePressed(MouseEvent ev) {
          this.xLastMouseMove = ev.getX();
          this.yLastMouseMove = ev.getY();
        }
  
        @Override
        public void mouseDragged(MouseEvent ev) {
          if (getModelNode() != null) {
            // Mouse move along X axis changes yaw 
            setViewYaw(getViewYaw() - ANGLE_FACTOR * (ev.getX() - this.xLastMouseMove));    
            this.xLastMouseMove = ev.getX();
            
            if (pitchAndScaleChangeSupported) {
              if (ev.isAltDown()) {
                // Mouse move along Y axis with Alt down changes scale
                setViewScale(Math.max(0.5f, Math.min(1.3f, getViewScale() * (float)Math.exp((ev.getY() - this.yLastMouseMove) * ZOOM_FACTOR))));
              } else {
                // Mouse move along Y axis changes pitch
                setViewPitch(Math.max(-(float)Math.PI / 4, Math.min(0, getViewPitch() - ANGLE_FACTOR * (ev.getY() - this.yLastMouseMove))));
              }
              this.yLastMouseMove = ev.getY();
            }
          }
        }
      };

    component3D.addMouseListener(mouseListener);
    component3D.addMouseMotionListener(mouseListener);
    
    if (pitchAndScaleChangeSupported) {
      component3D.addMouseWheelListener(new MouseWheelListener() {
          public void mouseWheelMoved(MouseWheelEvent ev) {
            // Mouse move along Y axis with Alt down changes scale
            setViewScale(Math.max(0.5f, Math.min(1.3f, getViewScale() * (float)Math.exp(ev.getWheelRotation() * ZOOM_FACTOR))));
          }
        });
    }
    
    // Redirect mouse events to the 3D component
    for (final MouseMotionListener l : getListeners(MouseMotionListener.class)) {
      component3D.addMouseMotionListener(new MouseMotionListener() {
          public void mouseMoved(MouseEvent ev) {
            l.mouseMoved(SwingUtilities.convertMouseEvent(component3D, ev, ModelPreviewComponent.this));
          }
          
          public void mouseDragged(MouseEvent ev) {
            l.mouseDragged(SwingUtilities.convertMouseEvent(component3D, ev, ModelPreviewComponent.this));
          }
        });
    }
    for (final MouseListener l : getListeners(MouseListener.class)) {
      component3D.addMouseListener(new MouseListener() {
          public void mouseReleased(MouseEvent ev) {
            l.mouseReleased(SwingUtilities.convertMouseEvent(component3D, ev, ModelPreviewComponent.this));
          }
          
          public void mousePressed(MouseEvent ev) {
            l.mousePressed(SwingUtilities.convertMouseEvent(component3D, ev, ModelPreviewComponent.this));
          }
          
          public void mouseExited(MouseEvent ev) {
            l.mouseExited(SwingUtilities.convertMouseEvent(component3D, ev, ModelPreviewComponent.this));
          }
          
          public void mouseEntered(MouseEvent ev) {
            l.mouseEntered(SwingUtilities.convertMouseEvent(component3D, ev, ModelPreviewComponent.this));
          }
          
          public void mouseClicked(MouseEvent ev) {
            l.mouseClicked(SwingUtilities.convertMouseEvent(component3D, ev, ModelPreviewComponent.this));
          }
        });
    }
  }

  /**
   * Creates universe bound to the 3D component.
   */
  private void createUniverse() {
    Canvas3D canvas3D;
    if (this.component3D instanceof Canvas3D) {
      canvas3D = (Canvas3D)this.component3D;
    } else {
      try {
        // Call JCanvas3D#getOffscreenCanvas3D by reflection to be able to run under Java 3D 1.3
        canvas3D = (Canvas3D)Class.forName("com.sun.j3d.exp.swing.JCanvas3D").getMethod("getOffscreenCanvas3D").invoke(this.component3D);
      } catch (Exception ex) {
        UnsupportedOperationException ex2 = new UnsupportedOperationException();
        ex2.initCause(ex);
        throw ex2;
      }
    }    
    // Create a universe bound to component 3D
    ViewingPlatform viewingPlatform = new ViewingPlatform();
    Viewer viewer = new Viewer(canvas3D);
    this.universe = new SimpleUniverse(viewingPlatform, viewer);
    // Set viewer location 
    updateViewPlatformTransform(this.universe.getViewingPlatform().getViewPlatformTransform(), 
        getViewYaw(), getViewPitch(), getViewScale());
    // Link scene to universe
    this.universe.addBranchGraph(this.sceneTree);

    revalidate();
    repaint();
    if (OperatingSystem.isMacOSX()) {
      final Component root = SwingUtilities.getRoot(this);
      EventQueue.invokeLater(new Runnable() {
          public void run() {
            // Request focus again even if dialog isn't supposed to have lost focus !
            if (KeyboardFocusManager.getCurrentKeyboardFocusManager().getActiveWindow() != root) {
              root.requestFocus();
            }
          }
        });
    } 
  }
  
  /**
   * Disposes universe bound to canvas.
   */
  private void disposeUniverse() {
    // Unlink scene to universe
    this.universe.getLocale().removeBranchGraph(this.sceneTree);
    this.universe.cleanup();
    this.universe = null;
  }
  
  /**
   * Creates a view bound to the universe that views current model from a point of view oriented with 
   * <code>yaw</code> and <code>pitch</code> angles.
   */
  View createView(float yaw, float pitch, float scale, int projectionPolicy) {
    if (this.universe == null) {
      createUniverse();
    }
    // Reuse same physical body and environment
    PhysicalBody physicalBody = this.universe.getViewer().getPhysicalBody();
    PhysicalEnvironment physicalEnvironment = this.universe.getViewer().getPhysicalEnvironment();
    
    // Create a view associated with canvas3D
    View view = new View();
    view.setPhysicalBody(physicalBody);
    view.setPhysicalEnvironment(physicalEnvironment);
    view.setProjectionPolicy(projectionPolicy);
    // Create a viewing platform and attach it to view and universe locale
    ViewingPlatform viewingPlatform = new ViewingPlatform();
    viewingPlatform.setUniverse(this.universe);
    this.universe.getLocale().addBranchGraph(
        (BranchGroup)viewingPlatform.getViewPlatformTransform().getParent());
    view.attachViewPlatform(viewingPlatform.getViewPlatform());

    // Set user point of view depending on yaw and pitch angles
    updateViewPlatformTransform(viewingPlatform.getViewPlatformTransform(), yaw, pitch, scale);
    return view;
  }
  
  /**
   * Returns the <code>yaw</code> angle used by view platform transform.
   */
  protected float getViewYaw() {
    return this.viewYaw;
  }
  
  /**
   * Sets the <code>yaw</code> angle used by view platform transform.
   */
  protected void setViewYaw(float viewYaw) {
    this.viewYaw = viewYaw;
    if (this.universe != null) {
      updateViewPlatformTransform(this.universe.getViewingPlatform().getViewPlatformTransform(), 
          getViewYaw(), getViewPitch(), getViewScale());
    }
  }

  /**
   * Returns the zoom factor used by view platform transform.
   */
  protected float getViewScale() {
    return this.viewScale;
  }
  
  /**
   * Sets the zoom factor used by view platform transform.
   */
  protected void setViewScale(float viewScale) {
    this.viewScale = viewScale;
    if (this.universe != null) {
      updateViewPlatformTransform(this.universe.getViewingPlatform().getViewPlatformTransform(), 
          getViewYaw(), getViewPitch(), getViewScale());
    }
  }

  /**
   * Returns the <code>pitch</code> angle used by view platform transform.
   */
  protected float getViewPitch() {
    return this.viewPitch;
  }

  /**
   * Sets the <code>pitch</code> angle used by view platform transform.
   */
  protected void setViewPitch(float viewPitch) {
    this.viewPitch = viewPitch;
    if (this.universe != null) {
      updateViewPlatformTransform(this.universe.getViewingPlatform().getViewPlatformTransform(), 
          getViewYaw(), getViewPitch(), getViewScale());
    }
  }

  /**
   * Updates the given view platform transformation from yaw angle, pitch angle and scale. 
   */
  private void updateViewPlatformTransform(TransformGroup viewPlatformTransform,
                                           float viewYaw, float viewPitch,
                                           float viewScale) {
    // Default distance used to view a 2 unit wide scene
    double nominalDistanceToCenter = 1.4 / Math.tan(Math.PI / 8);
    // We don't use a TransformGroup in scene tree to be able to share the same scene 
    // in the different views displayed by OrientationPreviewComponent class 
    Transform3D pitchRotation = new Transform3D();
    pitchRotation.rotX(viewPitch);
    Transform3D yawRotation = new Transform3D();
    yawRotation.rotY(viewYaw);
    Transform3D transform = new Transform3D();
    transform.setTranslation(
        new Vector3d(Math.sin(viewYaw) * nominalDistanceToCenter * Math.cos(viewPitch), 
            nominalDistanceToCenter * Math.sin(-viewPitch), 
            Math.cos(viewYaw) * nominalDistanceToCenter * Math.cos(viewPitch)));
    Transform3D scale = new Transform3D();
    scale.setScale(viewScale);
    
    yawRotation.mul(pitchRotation);
    transform.mul(yawRotation);
    scale.mul(transform);
    viewPlatformTransform.setTransform(scale);
  }
  
  /**
   * Returns scene tree root.
   */
  private BranchGroup createSceneTree() {
    BranchGroup root = new BranchGroup();
    root.setCapability(BranchGroup.ALLOW_DETACH);
    root.setCapability(BranchGroup.ALLOW_CHILDREN_READ);
    // Build scene tree
    root.addChild(createModelTree());
    root.addChild(createBackgroundNode());
    for (Light light : createLights()) {
      root.addChild(light);
    }
    return root;
  }
  
  /**
   * Returns the background node.  
   */
  private Node createBackgroundNode() {
    Background background = new Background(new Color3f(0.9f, 0.9f, 0.9f));
    background.setCapability(Background.ALLOW_COLOR_WRITE);
    background.setApplicationBounds(new BoundingSphere(new Point3d(0, 0, 0), 100));
    return background;
  }
  
  /**
   * Sets the background color.
   */
  public void setBackground(Color backgroundColor) {
    super.setBackground(backgroundColor);
    ((Background)this.sceneTree.getChild(1)).setColor(new Color3f(backgroundColor));
  }
  
  /**
   * Returns the lights of the scene.
   */
  private Light [] createLights() {
    Light [] lights = {
        new DirectionalLight(new Color3f(0.9f, 0.9f, 0.9f), new Vector3f(1.732f, -0.8f, -1)), 
        new DirectionalLight(new Color3f(0.9f, 0.9f, 0.9f), new Vector3f(-1.732f, -0.8f, -1)), 
        new DirectionalLight(new Color3f(0.9f, 0.9f, 0.9f), new Vector3f(0, -0.8f, 1)), 
        new DirectionalLight(new Color3f(0.66f, 0.66f, 0.66f), new Vector3f(0, 1f, 0)), 
        new AmbientLight(new Color3f(0.2f, 0.2f, 0.2f))}; 

    for (Light light : lights) {
      light.setInfluencingBounds(new BoundingSphere(new Point3d(0, 0, 0), 100));
    }
    return lights;
  }

  /**
   * Returns the root of model tree.
   */
  private Node createModelTree() {
    TransformGroup modelTransformGroup = new TransformGroup();      
    //  Allow transform group to have new children
    modelTransformGroup.setCapability(Group.ALLOW_CHILDREN_READ);
    modelTransformGroup.setCapability(Group.ALLOW_CHILDREN_WRITE);
    modelTransformGroup.setCapability(Group.ALLOW_CHILDREN_EXTEND);
    // Allow the change of the transformation that sets model size and position
    modelTransformGroup.setCapability(TransformGroup.ALLOW_TRANSFORM_WRITE);
    return modelTransformGroup;
  }

  /**
   * Returns the 3D model content displayed by this component.
   */
  public Content getModel() {
    return this.previewedPiece.getModel();
  }

  /**
   * Sets the 3D model content displayed by this component. 
   * The model is shown at its default orientation and in a box 1 unit wide.
   */
  public void setModel(final Content model) {
    final TransformGroup modelTransformGroup = (TransformGroup)this.sceneTree.getChild(0);
    modelTransformGroup.removeAllChildren();
    this.previewedPiece = null;
    this.pieceTextures.clear();
    if (model != null) {
      final AtomicReference<IllegalArgumentException> exception = new AtomicReference<IllegalArgumentException>();
      // Load content model synchronously (or get it from cache)
      ModelManager.getInstance().loadModel(model, true, new ModelManager.ModelObserver() {        
          public void modelUpdated(BranchGroup modelRoot) {
            if (modelRoot.numChildren() > 0) {
              try {
                Vector3f size = ModelManager.getInstance().getSize(modelRoot);
                previewedPiece = new HomePieceOfFurniture(
                    new CatalogPieceOfFurniture(null, null, model, 
                        size.x, size.z, size.y, 0, false, null, null, false, 0, false));
                previewedPiece.setX(0);
                previewedPiece.setY(0);
                previewedPiece.setElevation(-previewedPiece.getHeight() / 2);
                
                Transform3D modelTransform = new Transform3D();
                modelTransform.setScale(1.8 / Math.max(Math.max(size.x, size.z), size.y));
                modelTransformGroup.setTransform(modelTransform);

                modelTransformGroup.addChild(new HomePieceOfFurniture3D(previewedPiece, null, true, true));
              } catch (IllegalArgumentException ex) {
                // Model is empty
              }
            }
          }
          
          public void modelError(Exception ex) {
            exception.set(new IllegalArgumentException("Couldn't load model", ex));
          }
        });
      
      if (exception.get() != null) {
        throw exception.get(); 
      }
    }
  }

  /**
   * Sets the back face visibility of the children nodes of the displayed 3D model.
   */
  protected void setBackFaceShown(boolean backFaceShown) {
    if (this.previewedPiece != null) {
      // Create a new piece from the existing one with an updated backFaceShown flag 
      this.previewedPiece = new HomePieceOfFurniture(
          new CatalogPieceOfFurniture(null, null, this.previewedPiece.getModel(), 
              this.previewedPiece.getWidth(), 
              this.previewedPiece.getDepth(),
              this.previewedPiece.getHeight(),
              0, false, this.previewedPiece.getColor(), null, backFaceShown, 0, false));
      this.previewedPiece.setX(0);
      this.previewedPiece.setY(0);
      this.previewedPiece.setElevation(-previewedPiece.getHeight() / 2);
    
      TransformGroup modelTransformGroup = (TransformGroup)this.sceneTree.getChild(0);
      modelTransformGroup.addChild(new HomePieceOfFurniture3D(this.previewedPiece, null, true, true));
      if (modelTransformGroup.numChildren() > 1) {
        modelTransformGroup.removeChild(0);
      }
    }
  }

  /**
   * Returns the 3D model node displayed by this component. 
   */
  private HomePieceOfFurniture3D getModelNode() {
    TransformGroup modelTransformGroup = (TransformGroup)this.sceneTree.getChild(0);
    if (modelTransformGroup.numChildren() > 0) {
      return (HomePieceOfFurniture3D)modelTransformGroup.getChild(0);
    } else {
      return null;
    }
  }
  
  /**
   * Updates the rotation of the 3D model displayed by this component. 
   * The model is shown at its default size.
   */
  protected void setModelRotation(float [][] modelRotation) {
    BranchGroup modelNode = getModelNode();
    if (modelNode != null && modelNode.numChildren() > 0) {
      // Apply model rotation
      Transform3D rotationTransform = new Transform3D();
      if (modelRotation != null) {
        Matrix3f modelRotationMatrix = new Matrix3f(modelRotation [0][0], modelRotation [0][1], modelRotation [0][2],
            modelRotation [1][0], modelRotation [1][1], modelRotation [1][2],
            modelRotation [2][0], modelRotation [2][1], modelRotation [2][2]);
        rotationTransform.setRotation(modelRotationMatrix);
      }
      // Scale model to make it fit in a 1.8 unit wide box      
      Transform3D modelTransform = new Transform3D();
      Vector3f size = ModelManager.getInstance().getSize(modelNode);
      modelTransform.setScale(1.8 / Math.max(Math.max(size.x, size.z), size.y));
      modelTransform.mul(rotationTransform);
      
      TransformGroup modelTransformGroup = (TransformGroup)this.sceneTree.getChild(0);
      modelTransformGroup.setTransform(modelTransform);
    }
  }
  
  /**
   * Updates the rotation and the size of the 3D model displayed by this component. 
   */
  protected void setModelRotationAndSize(float [][] modelRotation,
                                         float width, float depth, float height) {
    BranchGroup modelNode = getModelNode();
    if (modelNode != null && modelNode.numChildren() > 0) {
      Transform3D normalization = ModelManager.getInstance().getNormalizedTransform(modelNode, modelRotation, 1f);
      // Scale model to its size
      Transform3D scaleTransform = new Transform3D();
      if (width != 0 && depth != 0 && height != 0) {
        scaleTransform.setScale(new Vector3d(width, height, depth));
      }
      scaleTransform.mul(normalization);
      // Scale model to make it fit in a 1.8 unit wide box      
      Transform3D modelTransform = new Transform3D();
      if (width != 0 && depth != 0 && height != 0) {
        modelTransform.setScale(1.8 / Math.max(Math.max(width, height), depth));
      } else {
        Vector3f size = ModelManager.getInstance().getSize(modelNode);
        modelTransform.setScale(1.8 / Math.max(Math.max(size.x, size.z), size.y));
      }
      modelTransform.mul(scaleTransform);
      
      TransformGroup modelTransformGroup = (TransformGroup)this.sceneTree.getChild(0);
      modelTransformGroup.setTransform(modelTransform);
    }
  }

  /**
   * Sets the color applied to 3D model.
   */
  protected void setModelColor(Integer color) {
    if (this.previewedPiece != null
        && this.previewedPiece.getColor() != color) {
      this.previewedPiece.setColor(color);
      getModelNode().update();
    }
  }

  /**
   * Sets the materials applied to 3D model.
   */
  public void setModelMaterials(HomeMaterial [] materials) {
    if (this.previewedPiece != null) {
      this.previewedPiece.setModelMaterials(materials);
      getModelNode().update();
      // Replace textures by clones because Java 3D doesn't accept all the time to share textures 
      cloneTexture(getModelNode(), this.pieceTextures);
    }
  }

  /**
   * Replace the textures set on <code>node</code> shapes by clones. 
   */
  private void cloneTexture(Node node, Map<Texture, Texture> replacedTextures) {
    if (node instanceof Group) {
      // Enumerate children
      Enumeration<?> enumeration = ((Group)node).getAllChildren(); 
      while (enumeration.hasMoreElements()) {
        cloneTexture((Node)enumeration.nextElement(), replacedTextures);
      }
    } else if (node instanceof Link) {
      cloneTexture(((Link)node).getSharedGroup(), replacedTextures);
    } else if (node instanceof Shape3D) {
      Appearance appearance = ((Shape3D)node).getAppearance();
      if (appearance != null) {
        Texture texture = appearance.getTexture();
        if (texture != null) {
          Texture replacedTexture = replacedTextures.get(texture);
          if (replacedTexture == null) {
            replacedTexture = (Texture)texture.cloneNodeComponent(false);
            replacedTextures.put(texture, replacedTexture);
          }
          appearance.setTexture(replacedTexture);
        }
      }
    } 
  }

  /**
   * Returns the icon image matching the displayed view.  
   */
  private BufferedImage getIconImage(int maxWaitingDelay) {
    Color backgroundColor = getBackground();
    
    BufferedImage imageWithWhiteBackgound = null;
    BufferedImage imageWithBlackBackgound = null;
    
    this.iconImageLock = new Object();
    try {
      // Instead of using off screen images that may cause some problems
      // use Robot to capture canvas 3D image 
      Point component3DOrigin = new Point();
      SwingUtilities.convertPointToScreen(component3DOrigin, this.component3D);
      
      Robot robot = new Robot();
      // Render scene with a white background
      if (this.iconImageLock != null) {
        synchronized (this.iconImageLock) {
          setBackground(Color.WHITE);
          try {
            this.iconImageLock.wait(maxWaitingDelay / 2);
          } catch (InterruptedException ex) {
            ex.printStackTrace();
          }
        }
      }
      imageWithWhiteBackgound = robot.createScreenCapture(
          new Rectangle(component3DOrigin, this.component3D.getSize()));
      
      // Render scene with a black background
      if (this.iconImageLock != null) {
        synchronized (this.iconImageLock) {
          setBackground(Color.BLACK);
          try {
            this.iconImageLock.wait(maxWaitingDelay / 2);
          } catch (InterruptedException ex) {
            ex.printStackTrace();
          }
        }
      }
      imageWithBlackBackgound = robot.createScreenCapture(
          new Rectangle(component3DOrigin, this.component3D.getSize()));
    } catch (AWTException ex) {
      throw new RuntimeException(ex);
    } finally {
      this.iconImageLock = null;
      setBackground(backgroundColor);
    }

    int [] imageWithWhiteBackgoundPixels = imageWithWhiteBackgound.getRGB(
        0, 0, imageWithWhiteBackgound.getWidth(), imageWithWhiteBackgound.getHeight(), null,
        0, imageWithWhiteBackgound.getWidth());
    int [] imageWithBlackBackgoundPixels = imageWithBlackBackgound.getRGB(
        0, 0, imageWithBlackBackgound.getWidth(), imageWithBlackBackgound.getHeight(), null,
        0, imageWithBlackBackgound.getWidth());
    
    // Create an image with transparent pixels where model isn't drawn
    for (int i = 0; i < imageWithBlackBackgoundPixels.length; i++) {
      if (imageWithBlackBackgoundPixels [i] != imageWithWhiteBackgoundPixels [i]
          && imageWithBlackBackgoundPixels [i] == 0xFF000000
          && imageWithWhiteBackgoundPixels [i] == 0xFFFFFFFF) {
        imageWithWhiteBackgoundPixels [i] = 0;
      }           
    }
    
    BufferedImage iconImage = new BufferedImage(imageWithWhiteBackgound.getWidth(), imageWithWhiteBackgound.getHeight(), 
        BufferedImage.TYPE_INT_ARGB);
    Graphics2D g2D = (Graphics2D)iconImage.getGraphics();
    g2D.drawImage(Toolkit.getDefaultToolkit().createImage(new MemoryImageSource(
        imageWithWhiteBackgound.getWidth(), imageWithWhiteBackgound.getHeight(), 
        imageWithWhiteBackgoundPixels, 0, imageWithWhiteBackgound.getWidth())), null, null);
    g2D.dispose();

    return iconImage;
  }

  /**
   * Returns a temporary content of the icon matching the displayed view.
   */
  public Content getIcon(int maxWaitingDelay) throws IOException {
    File tempIconFile = OperatingSystem.createTemporaryFile("icon", ".png");
    ImageIO.write(getIconImage(maxWaitingDelay), "png", tempIconFile);
    return new TemporaryURLContent(tempIconFile.toURI().toURL());
  }
  
  /**
   * Notifies the canvas 3D displayed by this component was swapped.
   */
  private void canvas3DSwapped() {
    if (this.iconImageLock != null) {
      synchronized (this.iconImageLock) {
        this.iconImageLock.notify();
      }
    }
  }
}