/*
 * Room3D.java 23 jan. 09
 *
 * Sweet Home 3D, Copyright (c) 2007-2009 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.j3d;

import java.awt.geom.Area;
import java.awt.geom.PathIterator;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.media.j3d.Appearance;
import javax.media.j3d.BranchGroup;
import javax.media.j3d.Geometry;
import javax.media.j3d.Node;
import javax.media.j3d.RenderingAttributes;
import javax.media.j3d.Shape3D;
import javax.media.j3d.Texture;
import javax.media.j3d.TextureAttributes;
import javax.media.j3d.TransparencyAttributes;
import javax.vecmath.Point3f;
import javax.vecmath.TexCoord2f;

import com.eteks.sweethome3d.model.Home;
import com.eteks.sweethome3d.model.HomeFurnitureGroup;
import com.eteks.sweethome3d.model.HomePieceOfFurniture;
import com.eteks.sweethome3d.model.HomeTexture;
import com.eteks.sweethome3d.model.Level;
import com.eteks.sweethome3d.model.Room;
import com.eteks.sweethome3d.model.Wall;
import com.sun.j3d.utils.geometry.GeometryInfo;
import com.sun.j3d.utils.geometry.NormalGenerator;

/**
 * Root of room branch.
 */
public class Room3D extends Object3DBranch {
  private static final TextureAttributes MODULATE_TEXTURE_ATTRIBUTES = new TextureAttributes();
  
  static {
    MODULATE_TEXTURE_ATTRIBUTES.setTextureMode(TextureAttributes.MODULATE);
  }
  
  private static final int FLOOR_PART  = 0;
  private static final int CEILING_PART = 1;
  
  private final Home home;

  /**
   * Creates the 3D room matching the given home <code>room</code>.
   */
  public Room3D(Room room, Home home) {
    this(room, home, false, false, false);
  }

  /**
   * Creates the 3D room matching the given home <code>room</code>.
   */
  public Room3D(Room room, Home home,
                boolean ignoreCeilingPart, 
                boolean ignoreInvisiblePart,
                boolean waitTextureLoadingEnd) {
    setUserData(room);
    this.home = home;

    // Allow room branch to be removed from its parent
    setCapability(BranchGroup.ALLOW_DETACH);
    // Allow to read branch shape children
    setCapability(BranchGroup.ALLOW_CHILDREN_READ);
    
    // Add room floor and cellar empty shapes to branch
    addChild(createRoomPartShape());
    addChild(createRoomPartShape());
    // Set room shape geometry and appearance
    updateRoomGeometry();
    updateRoomAppearance(waitTextureLoadingEnd);
    
    if (ignoreCeilingPart
        || (ignoreInvisiblePart
            && !room.isCeilingVisible())) {
      removeChild(CEILING_PART);
    }
    if (ignoreInvisiblePart
        && !room.isFloorVisible()) {
      removeChild(FLOOR_PART);
    }
  }

  /**
   * Returns a new room part shape with no geometry  
   * and a default appearance with a white material.
   */
  private Node createRoomPartShape() {
    Shape3D roomShape = new Shape3D();
    // Allow room shape to change its geometry
    roomShape.setCapability(Shape3D.ALLOW_GEOMETRY_WRITE);
    roomShape.setCapability(Shape3D.ALLOW_GEOMETRY_READ);
    roomShape.setCapability(Shape3D.ALLOW_APPEARANCE_READ);

    Appearance roomAppearance = new Appearance();
    roomShape.setAppearance(roomAppearance);
    roomAppearance.setCapability(Appearance.ALLOW_TRANSPARENCY_ATTRIBUTES_READ);
    TransparencyAttributes transparencyAttributes = new TransparencyAttributes();
    transparencyAttributes.setCapability(TransparencyAttributes.ALLOW_VALUE_WRITE);
    transparencyAttributes.setCapability(TransparencyAttributes.ALLOW_MODE_WRITE);
    roomAppearance.setTransparencyAttributes(transparencyAttributes);
    roomAppearance.setCapability(Appearance.ALLOW_RENDERING_ATTRIBUTES_READ);
    RenderingAttributes renderingAttributes = new RenderingAttributes();
    renderingAttributes.setCapability(RenderingAttributes.ALLOW_VISIBLE_WRITE);
    roomAppearance.setRenderingAttributes(renderingAttributes);
    roomAppearance.setCapability(Appearance.ALLOW_MATERIAL_WRITE);
    roomAppearance.setMaterial(DEFAULT_MATERIAL);      
    roomAppearance.setCapability(Appearance.ALLOW_TEXTURE_WRITE);
    roomAppearance.setCapability(Appearance.ALLOW_TEXTURE_READ);
    // Mix texture and room color
    roomAppearance.setTextureAttributes(MODULATE_TEXTURE_ATTRIBUTES);
    
    return roomShape;
  }

  @Override
  public void update() {
    updateRoomGeometry();
    updateRoomAppearance(false);
  }
  
  /**
   * Sets the 3D geometry of this room shapes that matches its 2D geometry.  
   */
  private void updateRoomGeometry() {
    updateRoomPartGeometry(FLOOR_PART, ((Room)getUserData()).getFloorTexture());
    updateRoomPartGeometry(CEILING_PART, ((Room)getUserData()).getCeilingTexture());
  }
  
  private void updateRoomPartGeometry(int roomPart, HomeTexture texture) {
    Shape3D roomShape = (Shape3D)getChild(roomPart);
    int currentGeometriesCount = roomShape.numGeometries();
    Room room = (Room)getUserData();
    if (room.getLevel() == null || room.getLevel().isVisible()) {
      for (Geometry roomGeometry : createRoomGeometries(roomPart, texture)) {
        roomShape.addGeometry(roomGeometry);
      }
    }
    for (int i = currentGeometriesCount - 1; i >= 0; i--) {
      roomShape.removeGeometry(i);
    }
  }
  
  /**
   * Returns room geometry computed from its points.
   */
  private Geometry [] createRoomGeometries(int roomPart, HomeTexture texture) {
    Room room = (Room)getUserData();
    float [][] points = room.getPoints();
    if ((roomPart == FLOOR_PART && room.isFloorVisible()
         || roomPart == CEILING_PART && room.isCeilingVisible())
        && points.length > 2) {
      Level roomLevel = room.getLevel();
      List<Level> levels = this.home.getLevels();
      boolean lastLevel = isLastLevel(roomLevel, levels);
      float floorBottomElevation;
      float roomElevation;
      if (roomLevel != null) {
        roomElevation = roomLevel.getElevation();
        floorBottomElevation = roomElevation - roomLevel.getFloorThickness();
      } else {
        roomElevation = 0;
        floorBottomElevation = 0;
      }

      float firstLevelElevation;
      if (levels.size() == 0) {
        firstLevelElevation = 0;
      } else {
        firstLevelElevation = levels.get(0).getElevation();
      }
      boolean floorBottomVisible = roomPart == FLOOR_PART 
          && roomLevel != null 
          && roomElevation != firstLevelElevation;

      // Find rooms at the same elevation 
      // and room ceilings at same elevation as the floor bottom  
      List<Room> roomsAtSameElevation = new ArrayList<Room>();
      List<Room> ceilingsAtSameFloorBottomElevation = new ArrayList<Room>();
      for (Room homeRoom : this.home.getRooms()) {
        Level homeRoomLevel = homeRoom.getLevel();
        if (room == homeRoom // Store also the room itself to know its order among rooms at same elevation
            || roomLevel == homeRoomLevel
                && (roomPart == FLOOR_PART && homeRoom.isFloorVisible()
                    || roomPart == CEILING_PART && homeRoom.isCeilingVisible()) 
            || roomLevel != null 
                && homeRoomLevel != null 
                && (roomPart == FLOOR_PART 
                        && homeRoom.isFloorVisible()
                        && Math.abs(roomElevation - homeRoomLevel.getElevation()) < 1E-4
                    || roomPart == CEILING_PART 
                        && homeRoom.isCeilingVisible()
                        && !lastLevel
                        && !isLastLevel(homeRoomLevel, levels)
                        && Math.abs(roomElevation + roomLevel.getHeight() - (homeRoomLevel.getElevation() + homeRoomLevel.getHeight())) < 1E-4)) {         
          roomsAtSameElevation.add(homeRoom);
        } else if (floorBottomVisible 
                    && homeRoomLevel != null 
                    && homeRoom.isCeilingVisible() 
                    && !isLastLevel(homeRoomLevel, levels) 
                    && Math.abs(floorBottomElevation - (homeRoomLevel.getElevation() + homeRoomLevel.getHeight())) < 1E-4) {
          ceilingsAtSameFloorBottomElevation.add(homeRoom);
        }
      }
      
      List<HomePieceOfFurniture> visibleStaircases;
      if (roomLevel == null
          || roomPart == CEILING_PART
              && lastLevel) {
        visibleStaircases = Collections.emptyList();
      } else {
        visibleStaircases = getVisibleStaircases(this.home.getFurniture(), roomPart, roomLevel, 
            roomLevel.getElevation() == firstLevelElevation);
      }

      // Check ceiling points of the last level are at the same elevation
      boolean sameElevation = true;
      if (roomPart == CEILING_PART
          && (roomLevel == null || lastLevel)) {
        float firstPointElevation = getRoomHeightAt(points [0][0], points [0][1]);
        for (int i = 1; i < points.length && sameElevation; i++) {
          sameElevation = getRoomHeightAt(points [i][0], points [i][1]) == firstPointElevation;
        }
      }
      
      // Retrieve room points
      List<float [][]> roomPoints;
      Map<Integer, List<float [][]>> roomHoles;
      Area roomVisibleArea;
      // If room isn't singular retrieve all the points of its different polygons 
      if (!room.isSingular() 
          || sameElevation
              && (roomsAtSameElevation.get(roomsAtSameElevation.size() - 1) != room
                  || visibleStaircases.size() > 0)) {        
        roomVisibleArea = new Area(getShape(points));
        if (roomsAtSameElevation.contains(room)) {
          // Remove other rooms surface that may overlap the current room
          for (int i = roomsAtSameElevation.size() - 1; i > 0 && roomsAtSameElevation.get(i) != room; i--) {
            Room otherRoom = roomsAtSameElevation.get(i);
            roomVisibleArea.subtract(new Area(getShape(otherRoom.getPoints())));
          }
        }        
        removeStaircasesFromArea(visibleStaircases, roomVisibleArea);
        roomPoints = new ArrayList<float[][]>();
        roomHoles = new HashMap<Integer, List<float [][]>>();
        getAreaPoints(roomVisibleArea, roomPart == CEILING_PART, roomPoints, roomHoles);
      } else {
        boolean clockwise = room.isClockwise();
        if (clockwise && roomPart == FLOOR_PART
            || !clockwise && roomPart == CEILING_PART) {
          // Reverse points order if they are in the good order
          points = getReversedArray(points);
        }
        roomPoints = Arrays.asList(new float [][][] {points});
        roomHoles = Collections.emptyMap();
        roomVisibleArea = null;
      }
      
      // Retrieve points of the room floor bottom
      List<float [][]> floorBottomPoints;
      Map<Integer, List<float [][]>> floorBottomHoles;
      if (floorBottomVisible) {
        if (roomVisibleArea != null 
            || ceilingsAtSameFloorBottomElevation.size() > 0) {        
          Area floorBottomVisibleArea = roomVisibleArea != null ? roomVisibleArea : new Area(getShape(points));
          // Remove other rooms surface that may overlap the floor bottom
          for (Room otherRoom : ceilingsAtSameFloorBottomElevation) {
            floorBottomVisibleArea.subtract(new Area(getShape(otherRoom.getPoints())));
          }          
          floorBottomPoints = new ArrayList<float[][]>();
          floorBottomHoles = new HashMap<Integer, List<float [][]>>();
          getAreaPoints(floorBottomVisibleArea, true, floorBottomPoints, floorBottomHoles);
        } else {
          floorBottomPoints = Arrays.asList(new float [][][] {getReversedArray(points)});
          floorBottomHoles = Collections.emptyMap();
        }
      } else {
        floorBottomPoints = Collections.emptyList();
        floorBottomHoles = Collections.emptyMap();
      }      
      
      List<Geometry> geometries = new ArrayList<Geometry> (roomPoints.size() + floorBottomPoints.size());
      boolean computeFloorBorder = roomLevel != null
          && roomPart == FLOOR_PART 
          && roomLevel.getElevation() != firstLevelElevation;
      // Compute room and border geometries
      int i = 0;      
      final float subpartSize = this.home.getEnvironment().getSubpartSizeUnderLight();
      for ( ; i < roomPoints.size(); i++) {
        float [][] roomPartPoints = roomPoints.get(i);
        float [] roomPartPointElevations = new float [roomPartPoints.length];
        boolean roomPartAtSameElevation = true;
        for (int j = 0; j < roomPartPoints.length; j++) {
          roomPartPointElevations [j] = roomPart == FLOOR_PART 
              ? roomElevation 
              : getRoomHeightAt(roomPartPoints [j][0], roomPartPoints [j][1]);
          if (roomPartAtSameElevation && j > 0) {
            roomPartAtSameElevation = roomPartPointElevations [j] == roomPartPointElevations [j - 1];
          }
        }
        
        if (roomPartAtSameElevation && subpartSize > 0) {
          // Subdivide area in smaller squares to ensure a smoother effect with point lights         
          float xMin = Float.MAX_VALUE;
          float xMax = Float.MIN_VALUE;
          float zMin = Float.MAX_VALUE;
          float zMax = Float.MIN_VALUE;
          for (float [] point : roomPartPoints) {
            xMin = Math.min(xMin, point [0]);
            xMax = Math.max(xMax, point [0]);
            zMin = Math.min(zMin, point [1]);
            zMax = Math.max(zMax, point [1]);
          }
          
          Area roomPartArea = new Area(getShape(roomPartPoints));        
          for (float xSquare = xMin; xSquare < xMax; xSquare += subpartSize) {
            for (float zSquare = zMin; zSquare < zMax; zSquare += subpartSize) {
              Area roomPartSquare = new Area(new Rectangle2D.Float(xSquare, zSquare, subpartSize, subpartSize));
              roomPartSquare.intersect(roomPartArea);
              List<float [][]> holes = roomHoles.get(i);
              if (holes != null) {
                for (float [][] geometryHole : holes) {
                  roomPartSquare.subtract(new Area(getShape(geometryHole)));
                }
              }
              if (!roomPartSquare.isEmpty()) {
                List<float [][]>               geometryPartPointsList = new ArrayList<float [][]>();
                Map<Integer, List<float [][]>> geometryPartHolesMap = new HashMap<Integer, List<float [][]>>();
                getAreaPoints(roomPartSquare, roomPart == CEILING_PART, geometryPartPointsList, geometryPartHolesMap);
                for (int j = 0 ; j < geometryPartPointsList.size(); j++) {
                  geometries.add(computeRoomPartGeometry(geometryPartPointsList.get(j), geometryPartHolesMap.get(j), 
                      null, roomLevel, roomPartPointElevations [0], floorBottomElevation, 
                      roomPart == FLOOR_PART, false, texture));
                }
              }
            }
          }
        } else {
          geometries.add(computeRoomPartGeometry(roomPartPoints, roomHoles.get(i), roomPartPointElevations, roomLevel,
              roomElevation, floorBottomElevation, roomPart == FLOOR_PART, false, texture));
        }
        
        if (computeFloorBorder) {
          geometries.add(computeRoomBorderGeometry(roomPartPoints, roomHoles.get(i),
              roomLevel, roomElevation, texture));
        }
      }

      // Compute floor bottom geometry
      for (i = 0 ; i < floorBottomPoints.size(); i++) {
        float [][] floorBottomPartPoints = floorBottomPoints.get(i);
        if (subpartSize > 0) {
          float xMin = Float.MAX_VALUE;
          float xMax = Float.MIN_VALUE;
          float zMin = Float.MAX_VALUE;
          float zMax = Float.MIN_VALUE;
          for (float [] point : floorBottomPartPoints) {
            xMin = Math.min(xMin, point [0]);
            xMax = Math.max(xMax, point [0]);
            zMin = Math.min(zMin, point [1]);
            zMax = Math.max(zMax, point [1]);
          }
          
          Area floorBottomPartArea = new Area(getShape(floorBottomPartPoints));
          for (float xSquare = xMin; xSquare < xMax; xSquare += subpartSize) {
            for (float zSquare = zMin; zSquare < zMax; zSquare += subpartSize) {
              Area floorBottomPartSquare = new Area(new Rectangle2D.Float(xSquare, zSquare, subpartSize, subpartSize));
              floorBottomPartSquare.intersect(floorBottomPartArea);
              List<float [][]> holes = floorBottomHoles.get(i);
              if (holes != null) {
                for (float [][] geometryHole : holes) {
                  floorBottomPartSquare.subtract(new Area(getShape(geometryHole)));
                }
              }
              if (!floorBottomPartSquare.isEmpty()) {
                List<float [][]>               geometryPartPointsList = new ArrayList<float [][]>();
                Map<Integer, List<float [][]>> geometryPartHolesMap = new HashMap<Integer, List<float [][]>>();
                getAreaPoints(floorBottomPartSquare, true, geometryPartPointsList, geometryPartHolesMap);
                for (int j = 0 ; j < geometryPartPointsList.size(); j++) {
                  geometries.add(computeRoomPartGeometry(geometryPartPointsList.get(j), geometryPartHolesMap.get(j), 
                      null, roomLevel, roomElevation, floorBottomElevation, 
                      true, true, texture));
                }
              }
            }
          }
        } else {
          geometries.add(computeRoomPartGeometry(floorBottomPartPoints, floorBottomHoles.get(i), null, roomLevel,
              roomElevation, floorBottomElevation, true, true, texture));
        }
      }

      return geometries.toArray(new Geometry [geometries.size()]);
    } else {
      return new Geometry [0];
    }
  }

  /**
   * Returns the room part geometry matching the given points.
   */
  private Geometry computeRoomPartGeometry(float [][] geometryPoints, 
                                           List<float [][]> geometryHoles,
                                           float [] roomPartPointElevations,
                                           Level roomLevel,
                                           float roomPartElevation, float floorBottomElevation,
                                           boolean floorPart, boolean floorBottomPart, 
                                           HomeTexture texture) {
    if (geometryHoles == null) {
      geometryHoles = Collections.emptyList();
    }
    int [] contourCounts = {1 + geometryHoles.size()};
    int [] stripCounts = new int [contourCounts.length + geometryHoles.size()];
    int i = 0;
    int vertexCount = geometryPoints.length;
    stripCounts [i++] = geometryPoints.length;
    for (float [][] geometryHole : geometryHoles) {
      vertexCount += geometryHole.length;
      stripCounts [i++] = geometryHole.length;
    }
    
    i = 0;
    Point3f [] coords = new Point3f [vertexCount];
    // Compute room coordinates
    for (int j = 0; j < geometryPoints.length; j++) {
      float y = floorBottomPart 
          ? floorBottomElevation 
          : (roomPartPointElevations != null
                ? roomPartPointElevations [j]
                : roomPartElevation);
      coords [i++] = new Point3f(geometryPoints [j][0], y, geometryPoints [j][1]);
    }
    for (float [][] geometryHole : geometryHoles) {
      for (int j = 0; j < geometryHole.length; j++) {
        float y = floorBottomPart
            ? floorBottomElevation
            : (floorPart
                  ? roomPartElevation 
                  : getRoomHeightAt(geometryHole [j][0], geometryHole [j][1]));
        coords [i++] = new Point3f(geometryHole [j][0], y, geometryHole [j][1]);
      }
    }
    GeometryInfo geometryInfo = new GeometryInfo(GeometryInfo.POLYGON_ARRAY);
    geometryInfo.setCoordinates(coords);
    geometryInfo.setStripCounts(stripCounts);
    geometryInfo.setContourCounts(contourCounts);
    
    if (texture != null) {
      TexCoord2f [] textureCoords = new TexCoord2f [vertexCount];
      i = 0;
      // Compute room texture coordinates
      for (int j = 0; j < geometryPoints.length; j++) {
        textureCoords [i++] = new TexCoord2f(geometryPoints [j][0] / texture.getWidth(), 
            floorPart 
                ? -geometryPoints [j][1] / texture.getHeight()
                : geometryPoints [j][1] / texture.getHeight());
      }
      for (float [][] geometryHole : geometryHoles) {
        for (int j = 0; j < geometryHole.length; j++) {
          textureCoords [i++] = new TexCoord2f(geometryHole [j][0] / texture.getWidth(), 
              floorPart 
                  ? -geometryHole [j][1] / texture.getHeight()
                  : geometryHole [j][1] / texture.getHeight());
        }
      }
      geometryInfo.setTextureCoordinateParams(1, 2);
      geometryInfo.setTextureCoordinates(0, textureCoords);
    }
    
    // Generate normals
    new NormalGenerator().generateNormals(geometryInfo);
    return geometryInfo.getIndexedGeometryArray();
  }

  /**
   * Returns the room border geometry matching the given points.
   */
  private Geometry computeRoomBorderGeometry(float [][] geometryPoints, 
                                             List<float [][]> geometryHoles,
                                             Level roomLevel, float roomElevation, 
                                             HomeTexture texture) {
    if (geometryHoles == null) {
      geometryHoles = Collections.emptyList();
    }
    int geometryHolesPointCount = 0;
    for (float [][] geometryHole : geometryHoles) {
      geometryHolesPointCount += geometryHole.length;
    }
    
    int [] contourCounts = new int [geometryPoints.length + geometryHolesPointCount];
    int [] stripCounts = new int [contourCounts.length];
    int vertexCount = geometryPoints.length * 4;
    vertexCount += geometryHolesPointCount * 4;
    Arrays.fill(contourCounts, 1);
    Arrays.fill(stripCounts, 4);

    int i = 0;
    Point3f [] coords = new Point3f [vertexCount];
    float floorBottomElevation = roomElevation - roomLevel.getFloorThickness();
    // Compute room borders coordinates
    for (int k = 0; k < geometryPoints.length; k++) {
      coords [i++] = new Point3f(geometryPoints [k][0], roomElevation, geometryPoints [k][1]);
      coords [i++] = new Point3f(geometryPoints [k][0], floorBottomElevation, geometryPoints [k][1]);
      int nextPoint = k < geometryPoints.length - 1  
          ? k + 1
          : 0;
      coords [i++] = new Point3f(geometryPoints [nextPoint][0], floorBottomElevation, geometryPoints [nextPoint][1]);
      coords [i++] = new Point3f(geometryPoints [nextPoint][0], roomElevation, geometryPoints [nextPoint][1]);
    }
    // Compute holes borders coordinates
    for (float [][] geometryHole : geometryHoles) {
      for (int k = 0; k < geometryHole.length; k++) {
        coords [i++] = new Point3f(geometryHole [k][0], roomElevation, geometryHole [k][1]);
        int nextPoint = k < geometryHole.length - 1  
            ? k + 1
            : 0;
        coords [i++] = new Point3f(geometryHole [nextPoint][0], roomElevation, geometryHole [nextPoint][1]);
        coords [i++] = new Point3f(geometryHole [nextPoint][0], floorBottomElevation, geometryHole [nextPoint][1]);
        coords [i++] = new Point3f(geometryHole [k][0], floorBottomElevation, geometryHole [k][1]);
      }
    }

    GeometryInfo geometryInfo = new GeometryInfo(GeometryInfo.POLYGON_ARRAY);
    geometryInfo.setCoordinates(coords);
    geometryInfo.setStripCounts(stripCounts);
    geometryInfo.setContourCounts(contourCounts);
    
    if (texture != null) {
      TexCoord2f [] textureCoords = new TexCoord2f [vertexCount];
      i = 0;
      // Compute room border texture coordinates
      for (int j = 0; j < geometryPoints.length; j++) {
        textureCoords [i++] = new TexCoord2f(geometryPoints [j][0] / texture.getWidth(), -geometryPoints [j][1] / texture.getHeight());
        textureCoords [i++] = new TexCoord2f(geometryPoints [j][0] / texture.getWidth(), -(geometryPoints [j][1] - roomLevel.getFloorThickness()) / texture.getHeight());
        int nextPoint = j < geometryPoints.length - 1  
            ? j + 1
            : 0;
        textureCoords [i++] = new TexCoord2f(geometryPoints [nextPoint][0] / texture.getWidth(), -(geometryPoints [nextPoint][1] - roomLevel.getFloorThickness()) / texture.getHeight());
        textureCoords [i++] = new TexCoord2f(geometryPoints [nextPoint][0] / texture.getWidth(), -geometryPoints [nextPoint][1] / texture.getHeight());
      }
      // Compute holes borders texture coordinates
      for (float [][] geometryHole : geometryHoles) {
        for (int j = 0; j < geometryHole.length; j++) {
          textureCoords [i++] = new TexCoord2f(geometryHole [j][0] / texture.getWidth(), -geometryHole [j][1] / texture.getHeight());
          int nextPoint = j < geometryHole.length - 1  
              ? j + 1
              : 0;
          textureCoords [i++] = new TexCoord2f(geometryHole [nextPoint][0] / texture.getWidth(), -geometryHole [nextPoint][1] / texture.getHeight());
          textureCoords [i++] = new TexCoord2f(geometryHole [nextPoint][0] / texture.getWidth(), -(geometryHole [nextPoint][1] - roomLevel.getFloorThickness()) / texture.getHeight());
          textureCoords [i++] = new TexCoord2f(geometryHole [j][0] / texture.getWidth(), -(geometryHole [j][1] - roomLevel.getFloorThickness()) / texture.getHeight());
        }
      }
      geometryInfo.setTextureCoordinateParams(1, 2);
      geometryInfo.setTextureCoordinates(0, textureCoords);
    }
    
    // Generate normals
    new NormalGenerator(Math.PI / 8).generateNormals(geometryInfo);
    return geometryInfo.getIndexedGeometryArray();
  }

  private void removeStaircasesFromArea(List<HomePieceOfFurniture> visibleStaircases, Area area) {
    // Remove from room area all the staircases that intersect it
    ModelManager modelManager = ModelManager.getInstance();
    for (HomePieceOfFurniture staircase : visibleStaircases) {
      area.subtract(modelManager.getAreaOnFloor(staircase));
    }
  }

  /**
   * Returns the visible staircases among the given <code>furniture</code>.  
   */
  private List<HomePieceOfFurniture> getVisibleStaircases(List<HomePieceOfFurniture> furniture, 
                                                          int roomPart, Level roomLevel,
                                                          boolean firstLevel) {
    List<HomePieceOfFurniture> visibleStaircases = new ArrayList<HomePieceOfFurniture>(furniture.size());
    for (HomePieceOfFurniture piece : furniture) {
      if (piece.isVisible()) {
        if (piece instanceof HomeFurnitureGroup) {
          visibleStaircases.addAll(getVisibleStaircases(((HomeFurnitureGroup)piece).getFurniture(), roomPart, roomLevel, firstLevel));
        } else if (piece.getStaircaseCutOutShape() != null
            && !"false".equalsIgnoreCase(piece.getStaircaseCutOutShape())
            && ((roomPart == FLOOR_PART 
                    && piece.getGroundElevation() < roomLevel.getElevation()
                    && piece.getGroundElevation() + piece.getHeight() >= roomLevel.getElevation() - (firstLevel ? 0 : roomLevel.getFloorThickness())
                || roomPart == CEILING_PART
                    && piece.getGroundElevation() < roomLevel.getElevation() + roomLevel.getHeight()
                    && piece.getGroundElevation() + piece.getHeight() >= roomLevel.getElevation() + roomLevel.getHeight()))) {
          visibleStaircases.add(piece);
        }
      }
    }
    return visibleStaircases;
  }

  /**
   * Fills <code>areaPoints</code> and <code>areaHoles</code> from the given area.
   */
  private void getAreaPoints(Area area, 
                             boolean reversed, 
                             List<float [][]> areaPoints,
                             Map<Integer, List<float [][]>> areaHoles) {
    // Retrieve the points of the different polygons 
    // and reverse their points order if necessary
    List<float []> currentPathPoints = new ArrayList<float[]>();
    float [] previousRoomPoint = null;
    int i = 0;
    for (PathIterator it = area.getPathIterator(null, 1); !it.isDone(); it.next()) {
      float [] roomPoint = new float[2];
      switch (it.currentSegment(roomPoint)) {
        case PathIterator.SEG_MOVETO :
        case PathIterator.SEG_LINETO : 
          if (previousRoomPoint == null
              || roomPoint [0] != previousRoomPoint [0] 
              || roomPoint [1] != previousRoomPoint [1]) {
            currentPathPoints.add(roomPoint);
          }
          previousRoomPoint = roomPoint;
          break;
        case PathIterator.SEG_CLOSE :
          if (currentPathPoints.get(0) [0] == previousRoomPoint [0] 
              && currentPathPoints.get(0) [1] == previousRoomPoint [1]) {
            currentPathPoints.remove(currentPathPoints.size() - 1);
          }
          if (currentPathPoints.size() > 2) {
            float [][] pathPoints = 
                currentPathPoints.toArray(new float [currentPathPoints.size()][]);
            Room subRoom = new Room(pathPoints);
            if (subRoom.getArea() > 0) {
              boolean pathPointsClockwise = subRoom.isClockwise();
              if (pathPointsClockwise) {
                // Store counter clockwise points as holes
                if (!reversed) {
                  pathPoints = getReversedArray(pathPoints);
                }
                List<float [][]> holes = areaHoles.get(i);
                if (holes == null) {
                  holes = new ArrayList<float [][]>(1);
                  areaHoles.put(i, holes);
                }
                holes.add(pathPoints);
              } else {
                if (reversed) {
                  pathPoints = getReversedArray(pathPoints);
                }
                areaPoints.add(pathPoints);
                i++;
              }
            }
          }
          currentPathPoints.clear();
          previousRoomPoint = null;
          break;
      }
    }
  }

  /**
   * Returns an array that cites <code>points</code> in reverse order.
   */
  private float [][] getReversedArray(float [][] points) {
    points = points.clone();
    List<float []> pointList = Arrays.asList(points);
    Collections.reverse(pointList);
    return pointList.toArray(points);
  }
  
  /**
   * Returns the room height at the given point. 
   */
  private float getRoomHeightAt(float x, float y) {
    double smallestDistance = Float.POSITIVE_INFINITY;
    Room room = (Room)getUserData();
    Level roomLevel = room.getLevel();
    float roomElevation = roomLevel != null
        ? roomLevel.getElevation()
        : 0;
    float roomHeight = roomElevation + 
        (roomLevel == null ? this.home.getWallHeight() : roomLevel.getHeight());
    List<Level> levels = this.home.getLevels();
    if (roomLevel == null || isLastLevel(roomLevel, levels)) {
      // Search the closest wall point to x, y at last level
      Wall closestWall = null;
      float [][] closestWallPoints = null;
      int closestIndex = -1;
      for (Wall wall : this.home.getWalls()) {
        if (wall.isAtLevel(roomLevel)) {
          float [][] points = wall.getPoints();
          for (int i = 0; i < points.length; i++) {
            double distanceToWallPoint = Point2D.distanceSq(points [i][0], points [i][1], x, y);
            if (distanceToWallPoint < smallestDistance) {
              closestWall = wall;
              closestWallPoints = points;
              closestIndex = i;
              smallestDistance = distanceToWallPoint;
            }
          }
        }
      }
      
      if (closestWall != null) {
        roomHeight = closestWall.getLevel() == null ? 0 : closestWall.getLevel().getElevation();
        Float wallHeightAtStart = closestWall.getHeight();
        if (closestIndex == 0 || closestIndex == closestWallPoints.length - 1) { // Wall start
          roomHeight += wallHeightAtStart != null 
              ? wallHeightAtStart 
              : this.home.getWallHeight();
        } else { // Wall end
          if (closestWall.isTrapezoidal()) {
            Float arcExtent = closestWall.getArcExtent();
            if (arcExtent == null
                || closestIndex == closestWallPoints.length / 2 
                || closestIndex == closestWallPoints.length / 2 - 1) {
              roomHeight += closestWall.getHeightAtEnd();
            } else {
              // Compute the angle between start point and the current point of the wall
              // to get the relative height at that point
              float xArcCircleCenter = closestWall.getXArcCircleCenter();
              float yArcCircleCenter = closestWall.getYArcCircleCenter();
              float xClosestPoint = closestWallPoints [closestIndex][0];
              float yClosestPoint = closestWallPoints [closestIndex][1];
              double centerToClosestPointDistance = Point2D.distance(xArcCircleCenter, yArcCircleCenter, xClosestPoint, yClosestPoint);
              float xStart = closestWall.getXStart();
              float yStart = closestWall.getYStart();
              double centerToStartPointDistance = Point2D.distance(xArcCircleCenter, yArcCircleCenter, xStart, yStart);
              double scalarProduct = (xClosestPoint - xArcCircleCenter) * (xStart - xArcCircleCenter) 
                  + (yClosestPoint - yArcCircleCenter) * (yStart - yArcCircleCenter);
              scalarProduct /= (centerToClosestPointDistance * centerToStartPointDistance);
              double arcExtentToClosestWallPoint = Math.acos(scalarProduct) * Math.signum(arcExtent);
              roomHeight += (float)(wallHeightAtStart 
                  + (closestWall.getHeightAtEnd() - wallHeightAtStart) * arcExtentToClosestWallPoint / arcExtent);
            }
          } else {
            roomHeight += (wallHeightAtStart != null ? wallHeightAtStart : this.home.getWallHeight());
          }
        }
      }
    }
    return roomHeight;
  }
  
  /**
   * Returns <code>true</code> if the given level is the last level in home.
   */
  private boolean isLastLevel(Level level, List<Level> levels) {
    return levels.indexOf(level) == levels.size() - 1;
  }

  /**
   * Sets room appearance with its color, texture.
   */
  private void updateRoomAppearance(boolean waitTextureLoadingEnd) {
    Room room = (Room)getUserData();
    boolean ignoreFloorTransparency = room.getLevel() == null || room.getLevel().getElevation() <= 0;
    updateRoomPartAppearance(((Shape3D)getChild(FLOOR_PART)).getAppearance(), 
        room.getFloorTexture(), waitTextureLoadingEnd, room.getFloorColor(), room.getFloorShininess(), room.isFloorVisible(), ignoreFloorTransparency);
    // Ignore ceiling transparency for rooms without level for backward compatibility 
    boolean ignoreCeillingTransparency = room.getLevel() == null; 
    updateRoomPartAppearance(((Shape3D)getChild(CEILING_PART)).getAppearance(), 
        room.getCeilingTexture(), waitTextureLoadingEnd, room.getCeilingColor(), room.getCeilingShininess(), room.isCeilingVisible(), ignoreCeillingTransparency);
  }
  
  /**
   * Sets room part appearance with its color, texture and visibility.
   */
  private void updateRoomPartAppearance(final Appearance roomPartAppearance, 
                                        final HomeTexture roomPartTexture,
                                        boolean waitTextureLoadingEnd,
                                        Integer roomPartColor,
                                        float shininess,
                                        boolean visible,
                                        boolean ignoreTransparency) {
    if (roomPartTexture == null) {
      roomPartAppearance.setMaterial(getMaterial(roomPartColor, roomPartColor, shininess));
      roomPartAppearance.setTexture(null);
    } else {
      // Update material and texture of room part
      roomPartAppearance.setMaterial(getMaterial(DEFAULT_COLOR, DEFAULT_AMBIENT_COLOR, shininess));
      final TextureManager textureManager = TextureManager.getInstance();
      textureManager.loadTexture(roomPartTexture.getImage(), waitTextureLoadingEnd,
          new TextureManager.TextureObserver() {
              public void textureUpdated(Texture texture) {
                roomPartAppearance.setTexture(texture);
              }
            });
    }
    if (!ignoreTransparency) { 
      // Update room part transparency
      float upperRoomsAlpha = this.home.getEnvironment().getWallsAlpha();
      TransparencyAttributes transparencyAttributes = roomPartAppearance.getTransparencyAttributes();
      transparencyAttributes.setTransparency(upperRoomsAlpha);
      // If alpha is equal to zero, turn off transparency to get better results 
      transparencyAttributes.setTransparencyMode(upperRoomsAlpha == 0 
          ? TransparencyAttributes.NONE 
          : TransparencyAttributes.NICEST);
    }
    // Update room part visibility
    RenderingAttributes renderingAttributes = roomPartAppearance.getRenderingAttributes();
    renderingAttributes.setVisible(visible);
  }
}