/*
 * WallController.java 30 mai 07
 *
 * Sweet Home 3D, Copyright (c) 2007 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.viewcontroller;

import java.awt.geom.Point2D;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.util.List;

import javax.swing.undo.AbstractUndoableEdit;
import javax.swing.undo.CannotRedoException;
import javax.swing.undo.CannotUndoException;
import javax.swing.undo.UndoableEdit;
import javax.swing.undo.UndoableEditSupport;

import com.eteks.sweethome3d.model.Home;
import com.eteks.sweethome3d.model.HomeTexture;
import com.eteks.sweethome3d.model.Selectable;
import com.eteks.sweethome3d.model.TextureImage;
import com.eteks.sweethome3d.model.UserPreferences;
import com.eteks.sweethome3d.model.Wall;

/**
 * A MVC controller for wall view.
 * @author Emmanuel Puybaret
 */
public class WallController implements Controller {
  /**
   * The properties that may be edited by the view associated to this controller. 
   */
  public enum Property {X_START, Y_START, X_END, Y_END, LENGTH, DISTANCE_TO_END_POINT, EDITABLE_POINTS, 
      LEFT_SIDE_COLOR, LEFT_SIDE_PAINT,  RIGHT_SIDE_COLOR, RIGHT_SIDE_PAINT, PATTERN, TOP_COLOR, TOP_PAINT,
      SHAPE, RECTANGULAR_WALL_HEIGHT, SLOPING_WALL_HEIGHT_AT_START, SLOPING_WALL_HEIGHT_AT_END, 
      THICKNESS, ARC_EXTENT_IN_DEGREES, LEFT_SIDE_SHININESS, RIGHT_SIDE_SHININESS}
  /**
   * The possible values for {@linkplain #getShape() wall shape}.
   */
  public enum WallShape {RECTANGULAR_WALL, SLOPING_WALL}
  /**
   * The possible values for {@linkplain #getLeftSidePaint() wall paint type}.
   */
  public enum WallPaint {DEFAULT, COLORED, TEXTURED} 

  private final Home                  home;
  private final UserPreferences       preferences;
  private final ViewFactory           viewFactory;
  private final ContentManager        contentManager;
  private final UndoableEditSupport   undoSupport;
  private TextureChoiceController     leftSideTextureController;
  private TextureChoiceController     rightSideTextureController;
  private final PropertyChangeSupport propertyChangeSupport;
  private DialogView                  wallView;

  private boolean      editablePoints;
  private Float        xStart;
  private Float        yStart;
  private Float        xEnd;
  private Float        yEnd;
  private Float        length;
  private Float        distanceToEndPoint;
  private Integer      leftSideColor;
  private WallPaint    leftSidePaint;
  private Float        leftSideShininess;
  private Integer      rightSideColor;
  private WallPaint    rightSidePaint;
  private Float        rightSideShininess;
  private TextureImage pattern;
  private Integer      topColor;
  private WallPaint    topPaint;
  private WallShape    shape;
  private Float        rectangularWallHeight;
  private Float        slopingWallHeightAtStart;
  private Float        sloppingWallHeightAtEnd;
  private Float        thickness;
  private Float        arcExtentInDegrees;

  /**
   * Creates the controller of wall view with undo support.
   */
  public WallController(final Home home, 
                        UserPreferences preferences,
                        ViewFactory viewFactory, 
                        ContentManager contentManager, 
                        UndoableEditSupport undoSupport) {
    this.home = home;
    this.preferences = preferences;
    this.viewFactory = viewFactory;
    this.contentManager = contentManager;
    this.undoSupport = undoSupport;
    this.propertyChangeSupport = new PropertyChangeSupport(this);
    
    updateProperties();
  }

  /**
   * Returns the texture controller of the wall left side.
   */
  public TextureChoiceController getLeftSideTextureController() {
    // Create sub controller lazily only once it's needed
    if (this.leftSideTextureController == null) {
      this.leftSideTextureController = new TextureChoiceController(
          this.preferences.getLocalizedString(WallController.class, "leftSideTextureTitle"), 
          this.preferences, this.viewFactory, this.contentManager);
      this.leftSideTextureController.addPropertyChangeListener(TextureChoiceController.Property.TEXTURE,
          new PropertyChangeListener() {
            public void propertyChange(PropertyChangeEvent ev) {
              setLeftSidePaint(WallPaint.TEXTURED);
            }
          });
    }
    return this.leftSideTextureController;
  }

  /**
   * Returns the texture controller of the wall right side.
   */
  public TextureChoiceController getRightSideTextureController() {
    // Create sub controller lazily only once it's needed
    if (this.rightSideTextureController == null) {
      this.rightSideTextureController = new TextureChoiceController(
          this.preferences.getLocalizedString(WallController.class, "rightSideTextureTitle"), 
          this.preferences, this.viewFactory, this.contentManager);
      this.rightSideTextureController.addPropertyChangeListener(TextureChoiceController.Property.TEXTURE,
          new PropertyChangeListener() {
            public void propertyChange(PropertyChangeEvent ev) {
              setRightSidePaint(WallPaint.TEXTURED);
            }
          });
    }
    return this.rightSideTextureController;
  }

  /**
   * Returns the view associated with this controller.
   */
  public DialogView getView() {
    // Create view lazily only once it's needed
    if (this.wallView == null) {
      this.wallView = this.viewFactory.createWallView(this.preferences, this); 
    }
    return this.wallView;
  }

  /**
   * Displays the view controlled by this controller.
   */
  public void displayView(View parentView) {
    getView().displayView(parentView);
  }

  /**
   * Adds the property change <code>listener</code> in parameter to this controller.
   */
  public void addPropertyChangeListener(Property property, PropertyChangeListener listener) {
    this.propertyChangeSupport.addPropertyChangeListener(property.name(), listener);
  }

  /**
   * Removes the property change <code>listener</code> in parameter from this controller.
   */
  public void removePropertyChangeListener(Property property, PropertyChangeListener listener) {
    this.propertyChangeSupport.removePropertyChangeListener(property.name(), listener);
  }

  /**
   * Updates edited properties from selected walls in the home edited by this controller.
   */
  protected void updateProperties() {
    List<Wall> selectedWalls = Home.getWallsSubList(this.home.getSelectedItems());
    if (selectedWalls.isEmpty()) {
      setXStart(null); // Nothing to edit
      setYStart(null); 
      setXEnd(null); 
      setYEnd(null);
      setEditablePoints(false);
      setLeftSideColor(null);
      getLeftSideTextureController().setTexture(null);
      setLeftSidePaint(null);
      setLeftSideShininess(null);
      setRightSideColor(null);
      getRightSideTextureController().setTexture(null);
      setRightSidePaint(null);
      setRightSideShininess(null);
      setRectangularWallHeight(null);
      setSlopingWallHeightAtStart(null);
      setSlopingWallHeightAtEnd(null);
      setShape(null);
      setThickness(null);
      setArcExtentInDegrees(null);
    } else {
      // Search the common properties among selected walls
      Wall firstWall = selectedWalls.get(0);
      boolean multipleSelection = selectedWalls.size() > 1;
      
      setEditablePoints(!multipleSelection);
      
      // Search the common xStart value among walls
      Float xStart = firstWall.getXStart();
      for (int i = 1; i < selectedWalls.size(); i++) {
        if (!xStart.equals(selectedWalls.get(i).getXStart())) {
          xStart = null;
          break;
        }
      }
      setXStart(xStart);      
      
      // Search the common yStart value among walls
      Float yStart = firstWall.getYStart();
      for (int i = 1; i < selectedWalls.size(); i++) {
        if (!yStart.equals(selectedWalls.get(i).getYStart())) {
          yStart = null;
          break;
        }
      }
      setYStart(yStart);      

      // Search the common xEnd value among walls
      Float xEnd = firstWall.getXEnd();
      for (int i = 1; i < selectedWalls.size(); i++) {
        if (!xEnd.equals(selectedWalls.get(i).getXEnd())) {
          xEnd = null;
          break;
        }
      }
      setXEnd(xEnd);      

      // Search the common yEnd value among walls
      Float yEnd = firstWall.getYEnd();
      for (int i = 1; i < selectedWalls.size(); i++) {
        if (!yEnd.equals(selectedWalls.get(i).getYEnd())) {
          yEnd = null;
          break;
        }
      }
      setYEnd(yEnd);      

      // Search the common left side color among walls
      Integer leftSideColor = firstWall.getLeftSideColor();
      if (leftSideColor != null) {
        for (int i = 1; i < selectedWalls.size(); i++) {
          if (!leftSideColor.equals(selectedWalls.get(i).getLeftSideColor())) {
            leftSideColor = null;
            break;
          }
        }
      }
      setLeftSideColor(leftSideColor);
      
      // Search the common left side texture among walls
      HomeTexture leftSideTexture = firstWall.getLeftSideTexture();
      if (leftSideTexture != null) {
        for (int i = 1; i < selectedWalls.size(); i++) {
          if (!leftSideTexture.equals(selectedWalls.get(i).getLeftSideTexture())) {
            leftSideTexture = null;
            break;
          }
        }
      }
      getLeftSideTextureController().setTexture(leftSideTexture);
      
      if (leftSideColor != null) {
        setLeftSidePaint(WallPaint.COLORED);
      } else if (leftSideTexture != null) {
        setLeftSidePaint(WallPaint.TEXTURED);
      } else {
        setLeftSidePaint(null);
      }
      
      // Search the common left side shininess value among walls
      Float leftSideShininess = firstWall.getLeftSideShininess();
      for (int i = 1; i < selectedWalls.size(); i++) {
        if (!leftSideShininess.equals(selectedWalls.get(i).getLeftSideShininess())) {
          leftSideShininess = null;
          break;
        }
      }
      setLeftSideShininess(leftSideShininess);      
      
      // Search the common right side color among walls
      Integer rightSideColor = firstWall.getRightSideColor();
      if (rightSideColor != null) {
        for (int i = 1; i < selectedWalls.size(); i++) {
          if (!rightSideColor.equals(selectedWalls.get(i).getRightSideColor())) {
            rightSideColor = null;
            break;
          }
        }
      }
      setRightSideColor(rightSideColor);
      
      // Search the common right side texture among walls
      HomeTexture rightSideTexture = firstWall.getRightSideTexture();
      if (rightSideTexture != null) {
        for (int i = 1; i < selectedWalls.size(); i++) {
          if (!rightSideTexture.equals(selectedWalls.get(i).getRightSideTexture())) {
            rightSideTexture = null;
            break;
          }
        }
      }
      getRightSideTextureController().setTexture(rightSideTexture);
      
      if (rightSideColor != null) {
        setRightSidePaint(WallPaint.COLORED);
      } else if (rightSideTexture != null) {
        setRightSidePaint(WallPaint.TEXTURED);
      } else {
        setRightSidePaint(null);
      }
      
      // Search the common right side shininess value among walls
      Float rightSideShininess = firstWall.getRightSideShininess();
      for (int i = 1; i < selectedWalls.size(); i++) {
        if (!rightSideShininess.equals(selectedWalls.get(i).getRightSideShininess())) {
          rightSideShininess = null;
          break;
        }
      }
      setRightSideShininess(rightSideShininess);      
      
      // Search the common pattern among walls
      TextureImage pattern = firstWall.getPattern();
      if (pattern == null) {
        pattern = this.preferences.getWallPattern(); 
      }
      for (int i = 1; i < selectedWalls.size(); i++) {
        TextureImage otherPattern = selectedWalls.get(i).getPattern();
        if (otherPattern == null) {
          otherPattern = this.preferences.getWallPattern(); 
        }
        if (!pattern.equals(otherPattern)) {
          pattern = null;
          break;
        }
      }
      setPattern(pattern);
      
      // Search the common top color among walls
      Integer topColor = firstWall.getTopColor();
      boolean defaultTopColor;
      if (topColor != null) {
        defaultTopColor = false;
        for (int i = 1; i < selectedWalls.size(); i++) {
          if (!topColor.equals(selectedWalls.get(i).getTopColor())) {
            topColor = null;
            break;
          }
        }
      } else {
        defaultTopColor = true;
        for (int i = 1; i < selectedWalls.size(); i++) {
          if (selectedWalls.get(i).getTopColor() != null) {
            defaultTopColor = false;
            break;
          }
        }
      }
      setTopColor(topColor);
      
      if (defaultTopColor) {
        setTopPaint(WallPaint.DEFAULT);
      } else if (topColor != null) {
        setTopPaint(WallPaint.COLORED);
      } else {
        setTopPaint(null);
      }
      
      // Search the common height among walls
      Float height = firstWall.getHeight();
      // If wall height was never set, use home wall height
      if (height == null && firstWall.getHeight() == null) {
        height = this.home.getWallHeight(); 
      }
      for (int i = 1; i < selectedWalls.size(); i++) {
        Wall wall = selectedWalls.get(i);
        float wallHeight = wall.getHeight() == null 
            ? this.home.getWallHeight()
            : wall.getHeight();  
        if (height != wallHeight) {
          height = null;
          break;
        }
      }
      setRectangularWallHeight(height);
      setSlopingWallHeightAtStart(height);
      
      // Search the common height at end among walls
      Float heightAtEnd = firstWall.getHeightAtEnd();
      if (heightAtEnd != null) {
        for (int i = 1; i < selectedWalls.size(); i++) {
          if (!heightAtEnd.equals(selectedWalls.get(i).getHeightAtEnd())) {
            heightAtEnd = null;
            break;
          }
        }
      }
      setSlopingWallHeightAtEnd(heightAtEnd == null && selectedWalls.size() == 1 ? height : heightAtEnd);
      
      boolean allWallsRectangular = !firstWall.isTrapezoidal();
      boolean allWallsTrapezoidal = firstWall.isTrapezoidal();
      for (int i = 1; i < selectedWalls.size(); i++) {
        if (!selectedWalls.get(i).isTrapezoidal()) {
          allWallsTrapezoidal = false;
        } else {
          allWallsRectangular = false;
        }
      }
      if (allWallsRectangular) {
        setShape(WallShape.RECTANGULAR_WALL);
      } else if (allWallsTrapezoidal) {
        setShape(WallShape.SLOPING_WALL);
      } else {
        setShape(null);
      }

      // Search the common thickness among walls
      Float thickness = firstWall.getThickness();
      for (int i = 1; i < selectedWalls.size(); i++) {
        if (thickness != selectedWalls.get(i).getThickness()) {
          thickness = null;
          break;
        }
      }
      setThickness(thickness);

      // Search the common arc extent among walls
      Float arcExtent = firstWall.getArcExtent();
      if (arcExtent != null) {
        for (int i = 1; i < selectedWalls.size(); i++) {
          if (!arcExtent.equals(selectedWalls.get(i).getArcExtent())) {
            arcExtent = null;
            break;
          }
        }
      }
      if (arcExtent != null) {
        setArcExtentInDegrees((float)Math.toDegrees(arcExtent));
      } else {
        setArcExtentInDegrees(selectedWalls.size() == 1 ? new Float(0) : null);
      }
    }
  }
  
  /**
   * Sets the edited abscissa of the start point.
   */
  public void setXStart(Float xStart) {
    if (xStart != this.xStart) {
      Float oldXStart = this.xStart;
      this.xStart = xStart;
      this.propertyChangeSupport.firePropertyChange(Property.X_START.name(), oldXStart, xStart);
      updateLength();
      updateDistanceToEndPoint();
    }
  }
  
  /**
   * Returns the edited abscissa of the start point.
   */
  public Float getXStart() {
    return this.xStart;
  }
  
  /**
   * Sets the edited ordinate of the start point.
   */
  public void setYStart(Float yStart) {
    if (yStart != this.yStart) {
      Float oldYStart = this.yStart;
      this.yStart = yStart;
      this.propertyChangeSupport.firePropertyChange(Property.Y_START.name(), oldYStart, yStart);
      updateLength();
      updateDistanceToEndPoint();
    }
  }
  
  /**
   * Returns the edited ordinate of the start point.
   */
  public Float getYStart() {
    return this.yStart;
  }
  
  /**
   * Sets the edited abscissa of the end point.
   */
  public void setXEnd(Float xEnd) {
    if (xEnd != this.xEnd) {
      Float oldXEnd = this.xEnd;
      this.xEnd = xEnd;
      this.propertyChangeSupport.firePropertyChange(Property.X_END.name(), oldXEnd, xEnd);
      updateLength();
      updateDistanceToEndPoint();
    }
  }
  
  /**
   * Returns the edited abscissa of the end point.
   */
  public Float getXEnd() {
    return this.xEnd;
  }
  
  /**
   * Sets the edited ordinate of the end point.
   */
  public void setYEnd(Float yEnd) {
    if (yEnd != this.yEnd) {
      Float oldYEnd = this.yEnd;
      this.yEnd = yEnd;
      this.propertyChangeSupport.firePropertyChange(Property.Y_END.name(), oldYEnd, yEnd);
      updateLength();
      updateDistanceToEndPoint();
    }
  }
  
  /**
   * Returns the edited ordinate of the end point.
   */
  public Float getYEnd() {
    return this.yEnd;
  }
  
  /**
   * Updates the edited length after its coordinates change.
   */
  private void updateLength() {
    Float xStart = getXStart();
    Float yStart = getYStart();
    Float xEnd = getXEnd();
    Float yEnd = getYEnd();    
    if (xStart != null && yStart != null && xEnd != null && yEnd != null) {
      Wall wall = new Wall(xStart, yStart, xEnd, yEnd, 0, 0);
      Float arcExtent = getArcExtentInDegrees();
      if (arcExtent != null) {
        wall.setArcExtent((float)Math.toRadians(arcExtent));
      }
      setLength(wall.getLength(), false);
    } else {
      setLength(null, false);
    }
  }
  
  /**
   * Sets the edited length.
   */
  public void setLength(Float length) {
    setLength(length, true);
  }

  /**
   * Returns the edited length.
   */
  public Float getLength() {
    return this.length;
  }
  
  /**
   * Sets the edited length and updates the coordinates of the end point if 
   * <code>updateEndPoint</code> is <code>true</code>.
   */
  private void setLength(Float length, boolean updateEndPoint) {
    if (length != this.length) {
      Float oldLength = this.length;
      this.length = length;
      this.propertyChangeSupport.firePropertyChange(Property.LENGTH.name(), oldLength, length);
      
      if (updateEndPoint) {
        Float xStart = getXStart();
        Float yStart = getYStart();
        Float xEnd = getXEnd();
        Float yEnd = getYEnd();
        if (xStart != null && yStart != null && xEnd != null && yEnd != null && length != null) {
          if (getArcExtentInDegrees() != null && getArcExtentInDegrees().floatValue() == 0) {
            double wallAngle = Math.atan2(yStart - yEnd, xEnd - xStart);
            setXEnd((float)(xStart + length * Math.cos(wallAngle)));
            setYEnd((float)(yStart - length * Math.sin(wallAngle)));
          } else {
            throw new UnsupportedOperationException(
                "Computing end point of a round wall from its length not supported");
          }
        } else {
          setXEnd(null);
          setYEnd(null);
        }
      }
    }
  }

  /**
   * Updates the edited distance to end point after its coordinates change.
   */
  private void updateDistanceToEndPoint() {
    Float xStart = getXStart();
    Float yStart = getYStart();
    Float xEnd = getXEnd();
    Float yEnd = getYEnd();    
    if (xStart != null && yStart != null && xEnd != null && yEnd != null) {
      setDistanceToEndPoint((float)Point2D.distance(xStart, yStart, xEnd, yEnd), false);
    } else {
      setDistanceToEndPoint(null, false);
    }
  }
  
  /**
   * Sets the edited distance to end point.
   */
  public void setDistanceToEndPoint(Float distanceToEndPoint) {
    setDistanceToEndPoint(distanceToEndPoint, true);
  }

  /**
   * Sets the edited distance to end point and updates the coordinates of the end point if 
   * <code>updateEndPoint</code> is <code>true</code>.
   */
  private void setDistanceToEndPoint(Float distanceToEndPoint, boolean updateEndPoint) {
    if (distanceToEndPoint != this.distanceToEndPoint) {
      Float oldDistance = this.distanceToEndPoint;
      this.distanceToEndPoint = distanceToEndPoint;
      this.propertyChangeSupport.firePropertyChange(Property.DISTANCE_TO_END_POINT.name(), oldDistance, distanceToEndPoint);
      
      if (updateEndPoint) {
        Float xStart = getXStart();
        Float yStart = getYStart();
        Float xEnd = getXEnd();
        Float yEnd = getYEnd();
        if (xStart != null && yStart != null && xEnd != null && yEnd != null && distanceToEndPoint != null) {
          double wallAngle = Math.atan2(yStart - yEnd, xEnd - xStart);
          setXEnd((float)(xStart + distanceToEndPoint * Math.cos(wallAngle)));
          setYEnd((float)(yStart - distanceToEndPoint * Math.sin(wallAngle)));
        } else {
          setXEnd(null);
          setYEnd(null);
        }
      }
    }
  }

  /**
   * Returns the edited distance to end point.
   */
  public Float getDistanceToEndPoint() {
    return this.distanceToEndPoint;
  }
  
  /**
   * Sets whether the point coordinates can be be edited or not.
   */
  public void setEditablePoints(boolean editablePoints) {
    if (editablePoints != this.editablePoints) {
      this.editablePoints = editablePoints;
      this.propertyChangeSupport.firePropertyChange(Property.EDITABLE_POINTS.name(), !editablePoints, editablePoints);
    }
  }
  
  /**
   * Returns whether the point coordinates can be be edited or not.
   */
  public boolean isEditablePoints() {
    return this.editablePoints;
  }
  
  /**
   * Sets the edited color of the left side.
   */
  public void setLeftSideColor(Integer leftSideColor) {
    if (leftSideColor != this.leftSideColor) {
      Integer oldLeftSideColor = this.leftSideColor;
      this.leftSideColor = leftSideColor;
      this.propertyChangeSupport.firePropertyChange(Property.LEFT_SIDE_COLOR.name(), oldLeftSideColor, leftSideColor);
      
      setLeftSidePaint(WallPaint.COLORED);
    }
  }
  
  /**
   * Returns the edited color of the left side.
   */
  public Integer getLeftSideColor() {
    return this.leftSideColor;
  }

  /**
   * Sets whether the left side is colored, textured or unknown painted.
   * @return {@link WallPaint#COLORED}, {@link WallPaint#TEXTURED} or <code>null</code>
   */
  public void setLeftSidePaint(WallPaint leftSidePaint) {
    if (leftSidePaint != this.leftSidePaint) {
      WallPaint oldLeftSidePaint = this.leftSidePaint;
      this.leftSidePaint = leftSidePaint;
      this.propertyChangeSupport.firePropertyChange(Property.LEFT_SIDE_PAINT.name(), oldLeftSidePaint, leftSidePaint);
    }
  }
  
  /**
   * Returns whether the left side is colored, textured or unknown painted.
   */
  public WallPaint getLeftSidePaint() {
    return this.leftSidePaint;
  }

  /**
   * Sets the edited left side shininess.
   */
  public void setLeftSideShininess(Float leftSideShininess) {
    if (leftSideShininess != this.leftSideShininess) {
      Float oldLeftSideShininess = this.leftSideShininess;
      this.leftSideShininess = leftSideShininess;
      this.propertyChangeSupport.firePropertyChange(Property.LEFT_SIDE_SHININESS.name(), oldLeftSideShininess, leftSideShininess);
    }
  }
  
  /**
   * Returns the edited left side shininess.
   */
  public Float getLeftSideShininess() {
    return this.leftSideShininess;
  }
  
  /**
   * Sets the edited color of the right side.
   */
  public void setRightSideColor(Integer rightSideColor) {
    if (rightSideColor != this.rightSideColor) {
      Integer oldRightSideColor = this.rightSideColor;
      this.rightSideColor = rightSideColor;
      this.propertyChangeSupport.firePropertyChange(Property.RIGHT_SIDE_COLOR.name(), oldRightSideColor, rightSideColor);
      
      setRightSidePaint(WallPaint.COLORED);
    }
  }
  
  /**
   * Returns the edited color of the right side.
   */
  public Integer getRightSideColor() {
    return this.rightSideColor;
  }

  /**
   * Sets whether the right side is colored, textured or unknown painted.
   */
  public void setRightSidePaint(WallPaint rightSidePaint) {
    if (rightSidePaint != this.rightSidePaint) {
      WallPaint oldRightSidePaint = this.rightSidePaint;
      this.rightSidePaint = rightSidePaint;
      this.propertyChangeSupport.firePropertyChange(Property.RIGHT_SIDE_PAINT.name(), oldRightSidePaint, rightSidePaint);
    }
  }
  
  /**
   * Returns whether the right side is colored, textured or unknown painted.
   * @return {@link WallPaint#COLORED}, {@link WallPaint#TEXTURED} or <code>null</code>
   */
  public WallPaint getRightSidePaint() {
    return this.rightSidePaint;
  }

  /**
   * Sets the edited right side shininess.
   */
  public void setRightSideShininess(Float rightSideShininess) {
    if (rightSideShininess != this.rightSideShininess) {
      Float oldRightSideShininess = this.rightSideShininess;
      this.rightSideShininess = rightSideShininess;
      this.propertyChangeSupport.firePropertyChange(Property.RIGHT_SIDE_SHININESS.name(), oldRightSideShininess, rightSideShininess);
    }
  }
  
  /**
   * Returns the edited right side shininess.
   */
  public Float getRightSideShininess() {
    return this.rightSideShininess;
  }
  
  /**
   * Sets the pattern of edited wall in plan, and notifies
   * listeners of this change.
   */
  public void setPattern(TextureImage pattern) {
    if (this.pattern != pattern) {
      TextureImage oldPattern = this.pattern;
      this.pattern = pattern;
      this.propertyChangeSupport.firePropertyChange(Property.PATTERN.name(), oldPattern, pattern);
    }
  }

  /**
   * Returns the pattern of edited wall in plan.
   */
  public TextureImage getPattern() {
    return this.pattern;
  }
  
  /**
   * Sets the edited top color in the 3D view.
   */
  public void setTopColor(Integer topColor) {
    if (topColor != this.topColor) {
      Integer oldTopColor = this.topColor;
      this.topColor = topColor;
      this.propertyChangeSupport.firePropertyChange(Property.TOP_COLOR.name(), oldTopColor, topColor);
    }
  }

  /**
   * Returns the edited top color in the 3D view.
   */
  public Integer getTopColor() {
    return this.topColor;
  }

  /**
   * Sets whether the top of the wall in the 3D view uses default rendering, is colored, or unknown painted.
   */
  public void setTopPaint(WallPaint topPaint) {
    if (topPaint != this.topPaint) {
      WallPaint oldTopPaint = this.topPaint;
      this.topPaint = topPaint;
      this.propertyChangeSupport.firePropertyChange(Property.TOP_PAINT.name(), oldTopPaint, topPaint);
    }
  }
  
  /**
   * Returns whether the top of the wall in the 3D view uses default rendering, is colored, or unknown painted.
   * @return {@link WallPaint#DEFAULT}, {@link WallPaint#COLORED} or <code>null</code>
   */
  public WallPaint getTopPaint() {
    return this.topPaint;
  }

  /**
   * Sets whether the edited wall is a rectangular wall, a sloping wall or unknown.
   */
  public void setShape(WallShape shape) {
    if (shape != this.shape) {
      WallShape oldShape = this.shape;
      this.shape = shape;
      this.propertyChangeSupport.firePropertyChange(Property.SHAPE.name(), oldShape, shape);
    }
  }
  
  /**
   * Returns whether the edited wall is a rectangular wall, a sloping wall or unknown.
   */
  public WallShape getShape() {
    return this.shape;
  }
  
  /**
   * Sets the edited height of a rectangular wall.
   */
  public void setRectangularWallHeight(Float rectangularWallHeight) {
    if (rectangularWallHeight != this.rectangularWallHeight) {
      Float oldRectangularWallHeight = this.rectangularWallHeight;
      this.rectangularWallHeight = rectangularWallHeight;
      this.propertyChangeSupport.firePropertyChange(Property.RECTANGULAR_WALL_HEIGHT.name(), 
          oldRectangularWallHeight, rectangularWallHeight);
      
      setShape(WallShape.RECTANGULAR_WALL);
    }
  }
  
  /**
   * Returns the edited height of a rectangular wall.
   */
  public Float getRectangularWallHeight() {
    return this.rectangularWallHeight;
  }
  
  /**
   * Sets the edited height at start of a sloping wall.
   */
  public void setSlopingWallHeightAtStart(Float slopingWallHeightAtStart) {
    if (slopingWallHeightAtStart != this.slopingWallHeightAtStart) {
      Float oldSlopingHeightHeightAtStart = this.slopingWallHeightAtStart;
      this.slopingWallHeightAtStart = slopingWallHeightAtStart;
      this.propertyChangeSupport.firePropertyChange(Property.SLOPING_WALL_HEIGHT_AT_START.name(), 
          oldSlopingHeightHeightAtStart, slopingWallHeightAtStart);
      
      setShape(WallShape.SLOPING_WALL);
    }
  }
  
  /**
   * Returns the edited height at start of a sloping wall.
   */
  public Float getSlopingWallHeightAtStart() {
    return this.slopingWallHeightAtStart;
  }
  
  /**
   * Sets the edited height at end of a sloping wall.
   */
  public void setSlopingWallHeightAtEnd(Float sloppingWallHeightAtEnd) {
    if (sloppingWallHeightAtEnd != this.sloppingWallHeightAtEnd) {
      Float oldSlopingWallHeightAtEnd = this.sloppingWallHeightAtEnd;
      this.sloppingWallHeightAtEnd = sloppingWallHeightAtEnd;
      this.propertyChangeSupport.firePropertyChange(Property.SLOPING_WALL_HEIGHT_AT_END.name(), 
          oldSlopingWallHeightAtEnd, sloppingWallHeightAtEnd);
      
      setShape(WallShape.SLOPING_WALL);
    }
  }
  
  /**
   * Returns the edited height at end of a sloping wall.
   */
  public Float getSlopingWallHeightAtEnd() {
    return this.sloppingWallHeightAtEnd;
  }
  
  /**
   * Sets the edited thickness.
   */
  public void setThickness(Float thickness) {
    if (thickness != this.thickness) {
      Float oldThickness = this.thickness;
      this.thickness = thickness;
      this.propertyChangeSupport.firePropertyChange(Property.THICKNESS.name(), oldThickness, thickness);
    }
  }
  
  /**
   * Returns the edited thickness.
   */
  public Float getThickness() {
    return this.thickness;
  }
  
  /**
   * Sets the edited arc extent.
   */
  public void setArcExtentInDegrees(Float arcExtentInDegrees) {
    if (arcExtentInDegrees != this.arcExtentInDegrees) {
      Float oldArcExtent = this.arcExtentInDegrees;
      this.arcExtentInDegrees = arcExtentInDegrees;
      this.propertyChangeSupport.firePropertyChange(Property.ARC_EXTENT_IN_DEGREES.name(), oldArcExtent, arcExtentInDegrees);
    }
  }
  
  /**
   * Returns the edited arc extent.
   */
  public Float getArcExtentInDegrees() {
    return this.arcExtentInDegrees;
  }
  
  /**
   * Controls the modification of selected walls in edited home.
   */
  public void modifyWalls() {
    List<Selectable> oldSelection = this.home.getSelectedItems(); 
    List<Wall> selectedWalls = Home.getWallsSubList(oldSelection);
    if (!selectedWalls.isEmpty()) {
      Float xStart = getXStart();
      Float yStart = getYStart();
      Float xEnd = getXEnd();
      Float yEnd = getYEnd();
      Integer leftSideColor = getLeftSidePaint() == WallPaint.COLORED 
          ? getLeftSideColor() : null;
      HomeTexture leftSideTexture = getLeftSidePaint() == WallPaint.TEXTURED
          ? getLeftSideTextureController().getTexture() : null;
      Float leftSideShininess = getLeftSideShininess();
      Integer rightSideColor = getRightSidePaint() == WallPaint.COLORED
          ? getRightSideColor() : null;
      HomeTexture rightSideTexture = getRightSidePaint() == WallPaint.TEXTURED
          ? getRightSideTextureController().getTexture() : null;
      TextureImage pattern = getPattern();
      boolean modifiedTopColor = getTopPaint() != null;
      Integer topColor = getTopPaint() == WallPaint.COLORED
          ? getTopColor() : null;
      Float rightSideShininess = getRightSideShininess();
      Float thickness = getThickness();
      Float arcExtent = getArcExtentInDegrees();
      if (arcExtent != null) {
        arcExtent = (float)Math.toRadians(arcExtent);
      }
      Float height;
      if (getShape() == WallShape.SLOPING_WALL) {
        height = getSlopingWallHeightAtStart();
      } else if (getShape() == WallShape.RECTANGULAR_WALL) {
        height = getRectangularWallHeight();
      } else {
        height = null;
      }
      Float heightAtEnd;
      if (getShape() == WallShape.SLOPING_WALL) {
        heightAtEnd = getSlopingWallHeightAtEnd();
      } else if (getShape() == WallShape.RECTANGULAR_WALL) {
        heightAtEnd = getRectangularWallHeight();
      } else {
        heightAtEnd = null;
      }
      
      // Create an array of modified walls with their current properties values
      ModifiedWall [] modifiedWalls = new ModifiedWall [selectedWalls.size()]; 
      for (int i = 0; i < modifiedWalls.length; i++) {
        modifiedWalls [i] = new ModifiedWall(selectedWalls.get(i));
      }
      // Apply modification
      doModifyWalls(modifiedWalls, xStart, yStart, xEnd, yEnd, 
          leftSideColor, leftSideTexture, leftSideShininess, 
          rightSideColor, rightSideTexture, rightSideShininess,
          pattern, modifiedTopColor, topColor,
          height, heightAtEnd, thickness, arcExtent);      
      if (this.undoSupport != null) {
        UndoableEdit undoableEdit = new WallsModificationUndoableEdit(this.home, 
            this.preferences, oldSelection,
            modifiedWalls, xStart, yStart, xEnd, yEnd,
            leftSideColor, leftSideTexture, leftSideShininess, 
            rightSideColor, rightSideTexture, rightSideShininess,
            pattern, modifiedTopColor, topColor,
            height, heightAtEnd, thickness, arcExtent);
        this.undoSupport.postEdit(undoableEdit);
      }
    }
  }

  /**
   * Undoable edit for walls modification. This class isn't anonymous to avoid
   * being bound to controller and its view.
   */
  private static class WallsModificationUndoableEdit extends AbstractUndoableEdit {
    private final Home             home;
    private final UserPreferences  preferences;
    private final List<Selectable> oldSelection;
    private final ModifiedWall []  modifiedWalls;
    private final Float            xStart;
    private final Float            yStart;
    private final Float            xEnd;
    private final Float            yEnd;
    private final Integer          leftSideColor;
    private final HomeTexture      leftSideTexture;
    private final Float            leftSideShininess;
    private final Integer          rightSideColor;
    private final HomeTexture      rightSideTexture;
    private final Float            rightSideShininess;
    private final TextureImage     pattern;
    private final boolean          modifiedTopColor;
    private final Integer          topColor;
    private final Float            height;
    private final Float            heightAtEnd;
    private final Float            thickness;
    private final Float            arcExtent;

    private WallsModificationUndoableEdit(Home home,
                                          UserPreferences preferences,
                                          List<Selectable> oldSelection,
                                          ModifiedWall [] modifiedWalls,
                                          Float xStart, Float yStart,
                                          Float xEnd, Float yEnd,
                                          Integer leftSideColor,
                                          HomeTexture leftSideTexture,
                                          Float leftSideShininess,
                                          Integer rightSideColor,
                                          HomeTexture rightSideTexture,
                                          Float rightSideShininess,
                                          TextureImage pattern,
                                          boolean modifiedTopColor,
                                          Integer topColor, 
                                          Float height,
                                          Float heightAtEnd,
                                          Float thickness,
                                          Float arcExtent) {
      this.home = home;
      this.preferences = preferences;
      this.oldSelection = oldSelection;
      this.modifiedWalls = modifiedWalls;
      this.xStart = xStart;
      this.yStart = yStart;
      this.xEnd = xEnd;
      this.yEnd = yEnd;
      this.leftSideColor = leftSideColor;
      this.rightSideTexture = rightSideTexture;
      this.leftSideShininess = leftSideShininess;
      this.rightSideColor = rightSideColor;
      this.leftSideTexture = leftSideTexture;
      this.rightSideShininess = rightSideShininess;
      this.pattern = pattern;
      this.modifiedTopColor = modifiedTopColor;
      this.topColor = topColor;
      this.height = height;
      this.heightAtEnd = heightAtEnd;
      this.thickness = thickness;
      this.arcExtent = arcExtent;
    }

    @Override
    public void undo() throws CannotUndoException {
      super.undo();
      undoModifyWalls(this.modifiedWalls); 
      this.home.setSelectedItems(this.oldSelection); 
    }

    @Override
    public void redo() throws CannotRedoException {
      super.redo();
      doModifyWalls(this.modifiedWalls, this.xStart, this.yStart, this.xEnd, this.yEnd, 
          this.leftSideColor, this.leftSideTexture, this.leftSideShininess, 
          this.rightSideColor, this.rightSideTexture, this.rightSideShininess,
          this.pattern, this.modifiedTopColor, this.topColor,
          this.height, this.heightAtEnd, this.thickness, this.arcExtent); 
      this.home.setSelectedItems(this.oldSelection); 
    }

    @Override
    public String getPresentationName() {
      return this.preferences.getLocalizedString(WallController.class, "undoModifyWallsName");
    }
  }

  /**
   * Modifies walls properties with the values in parameter.
   */
  private static void doModifyWalls(ModifiedWall [] modifiedWalls, 
                                    Float xStart, Float yStart, Float xEnd, Float yEnd,
                                    Integer leftSideColor, HomeTexture leftSideTexture, Float leftSideShininess,
                                    Integer rightSideColor, HomeTexture rightSideTexture, Float rightSideShininess,
                                    TextureImage pattern, boolean modifiedTopColor, Integer topColor, 
                                    Float height, Float heightAtEnd, Float thickness, Float arcExtent) {
    for (ModifiedWall modifiedWall : modifiedWalls) {
      Wall wall = modifiedWall.getWall();
      moveWallPoints(wall, xStart, yStart, xEnd, yEnd);
      if (leftSideTexture != null) {
        wall.setLeftSideTexture(leftSideTexture);
        wall.setLeftSideColor(null);
      } else if (leftSideColor != null) {
        wall.setLeftSideColor(leftSideColor);
        wall.setLeftSideTexture(null);
      }
      if (leftSideShininess != null) {
        wall.setLeftSideShininess(leftSideShininess);
      }
      if (rightSideTexture != null) {
        wall.setRightSideTexture(rightSideTexture);
        wall.setRightSideColor(null);
      } else if (rightSideColor != null) {
        wall.setRightSideColor(rightSideColor);
        wall.setRightSideTexture(null);
      }
      if (rightSideShininess != null) {
        wall.setRightSideShininess(rightSideShininess);
      }
      if (pattern != null) {
        wall.setPattern(pattern);
      }
      if (modifiedTopColor) {
        wall.setTopColor(topColor);
      }
      if (height != null) {
        wall.setHeight(height);
        if (heightAtEnd != null) {
          if (heightAtEnd.equals(height)) {
            wall.setHeightAtEnd(null);
          } else {
            wall.setHeightAtEnd(heightAtEnd);
          }
        }
      }
      if (thickness != null) {
        wall.setThickness(thickness.floatValue());
      }
      if (arcExtent != null) {
        if (arcExtent.floatValue() == 0) {
          wall.setArcExtent(null);
        } else {
          wall.setArcExtent(arcExtent);
        }
      }
    }
  }

  /**
   * Restores wall properties from the values stored in <code>modifiedWalls</code>.
   */
  private static void undoModifyWalls(ModifiedWall [] modifiedWalls) {
    for (ModifiedWall modifiedWall : modifiedWalls) {
      Wall wall = modifiedWall.getWall();
      moveWallPoints(wall, modifiedWall.getXStart(), modifiedWall.getYStart(),
          modifiedWall.getXEnd(), modifiedWall.getYEnd());
      wall.setLeftSideColor(modifiedWall.getLeftSideColor());
      wall.setLeftSideTexture(modifiedWall.getLeftSideTexture());
      wall.setLeftSideShininess(modifiedWall.getLeftSideShininess());
      wall.setRightSideColor(modifiedWall.getRightSideColor());
      wall.setRightSideTexture(modifiedWall.getRightSideTexture());
      wall.setRightSideShininess(modifiedWall.getRightSideShininess());
      wall.setPattern(modifiedWall.getPattern());
      wall.setTopColor(modifiedWall.getTopColor());
      wall.setHeight(modifiedWall.getHeight());
      wall.setHeightAtEnd(modifiedWall.getHeightAtEnd());
      wall.setThickness(modifiedWall.getThickness());
      wall.setArcExtent(modifiedWall.getArcExtent());
    }
  }
  
  private static void moveWallPoints(Wall wall, Float xStart, Float yStart, Float xEnd, Float yEnd) {
    Wall wallAtStart = wall.getWallAtStart();
    if (xStart != null) {
      wall.setXStart(xStart);
      // If wall is joined to a wall at its start 
      if (wallAtStart != null) {
        // Move the wall start point or end point
        if (wallAtStart.getWallAtStart() == wall) {
          wallAtStart.setXStart(xStart);
        } else if (wallAtStart.getWallAtEnd() == wall) {
          wallAtStart.setXEnd(xStart);
        }
      }
    }
    if (yStart != null) {
      wall.setYStart(yStart);
      // If wall is joined to a wall at its start 
      if (wallAtStart != null) {
        // Move the wall start point or end point
        if (wallAtStart.getWallAtStart() == wall) {
          wallAtStart.setYStart(yStart);
        } else if (wallAtStart.getWallAtEnd() == wall) {
          wallAtStart.setYEnd(yStart);
        }
      }
    }
    Wall wallAtEnd = wall.getWallAtEnd();
    if (xEnd != null) {
      wall.setXEnd(xEnd);
      // If wall is joined to a wall at its end  
      if (wallAtEnd != null) {
        // Move the wall start point or end point
        if (wallAtEnd.getWallAtStart() == wall) {
          wallAtEnd.setXStart(xEnd);
        } else if (wallAtEnd.getWallAtEnd() == wall) {
          wallAtEnd.setXEnd(xEnd);
        }
      }
    }
    if (yEnd != null) {
      wall.setYEnd(yEnd);
      // If wall is joined to a wall at its end  
      if (wallAtEnd != null) {
        // Move the wall start point or end point
        if (wallAtEnd.getWallAtStart() == wall) {
          wallAtEnd.setYStart(yEnd);
        } else if (wallAtEnd.getWallAtEnd() == wall) {
          wallAtEnd.setYEnd(yEnd);
        }
      }
    }
  }

  /**
   * Stores the current properties values of a modified wall.
   */
  private static final class ModifiedWall {
    private final Wall         wall;
    private final float        xStart;
    private final float        yStart;
    private final float        xEnd;
    private final float        yEnd;
    private final Integer      leftSideColor;
    private final HomeTexture  leftSideTexture;
    private final float        leftSideShininess;
    private final Integer      rightSideColor;
    private final HomeTexture  rightSideTexture;
    private final float        rightSideShininess;
    private final TextureImage pattern;
    private final Integer      topColor;
    private final Float        height;
    private final Float        heightAtEnd;
    private final float        thickness;
    private final Float        arcExtent;

    public ModifiedWall(Wall wall) {
      this.wall = wall;
      this.xStart = wall.getXStart();
      this.yStart = wall.getYStart();
      this.xEnd = wall.getXEnd();
      this.yEnd = wall.getYEnd();
      this.leftSideColor = wall.getLeftSideColor();
      this.leftSideTexture = wall.getLeftSideTexture();
      this.leftSideShininess = wall.getLeftSideShininess();
      this.rightSideColor = wall.getRightSideColor();
      this.rightSideTexture = wall.getRightSideTexture();
      this.rightSideShininess = wall.getRightSideShininess();
      this.pattern = wall.getPattern();
      this.topColor = wall.getTopColor();
      this.height = wall.getHeight();
      this.heightAtEnd = wall.getHeightAtEnd();
      this.thickness = wall.getThickness();
      this.arcExtent = wall.getArcExtent();
    }

    public Wall getWall() {
      return this.wall;
    }
    
    public float getXStart() {
      return this.xStart;
    }
    
    public float getXEnd() {
      return this.xEnd;
    }
    
    public float getYStart() {
      return this.yStart;
    }    
    
    public float getYEnd() {
      return this.yEnd;
    }
    
    public Float getHeight() {
      return this.height;
    }
    
    public Float getHeightAtEnd() {
      return this.heightAtEnd;
    }
    
    public Integer getLeftSideColor() {
      return this.leftSideColor;
    }
    
    public HomeTexture getLeftSideTexture() {
      return this.leftSideTexture;
    }
    
    public float getLeftSideShininess() {
      return this.leftSideShininess;
    }

    public Integer getRightSideColor() {
      return this.rightSideColor;
    }
    
    public HomeTexture getRightSideTexture() {
      return this.rightSideTexture;
    }
    
    public float getRightSideShininess() {
      return this.rightSideShininess;
    }
    
    public TextureImage getPattern() {
      return this.pattern;
    }
    
    public Integer getTopColor() {
      return this.topColor;
    }
    
    public float getThickness() {
      return this.thickness;
    }

    public Float getArcExtent() {
      return this.arcExtent;
    }
  }
}
