/*
 * HomeFurnitureGroup.java 4 févr. 2010
 *
 * Sweet Home 3D, Copyright (c) 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.model;

import java.awt.geom.AffineTransform;
import java.awt.geom.GeneralPath;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;

/**
 * A group of furniture of furniture.
 * @since 2.3
 * @author Emmanuel Puybaret
 */
public class HomeFurnitureGroup extends HomePieceOfFurniture {
  private static final long serialVersionUID = 1L;
  
  private static final float [][] IDENTITY = new float [][] {{1, 0, 0}, {0, 1, 0}, {0, 0, 1}};

  private List<HomePieceOfFurniture> furniture;
  private boolean                    resizable;
  private boolean                    deformable;
  private boolean                    texturable;
  private boolean                    doorOrWindow;
  private float                      fixedWidth;
  private float                      fixedDepth;
  private float                      fixedHeight;
  private String                     currency;
  private List<Integer>              furnitureDefaultColors;
  private List<HomeTexture>          furnitureDefaultTextures;

  /**
   * Creates a group from the given <code>furniture</code> list. 
   * The level of each piece of furniture of the group will be reset to <code>null</code> and if they belong to levels
   * with different elevations, their elevation will be updated to be relative to the elevation of the lowest level.
   */
  public HomeFurnitureGroup(List<HomePieceOfFurniture> furniture,
                            String name) {
    super(furniture.get(0));
    this.furniture = Collections.unmodifiableList(furniture); 
    
    // Search the size of the furniture group
    HomePieceOfFurniture firstPiece = furniture.get(0);
    AffineTransform rotation = AffineTransform.getRotateInstance(-firstPiece.getAngle());
    Rectangle2D unrotatedBoundingRectangle = null;
    for (HomePieceOfFurniture piece : getFurnitureWithoutGroups(furniture)) {
      GeneralPath pieceShape = new GeneralPath();
      float [][] points = piece.getPoints();
      pieceShape.moveTo(points [0][0], points [0][1]);
      for (int i = 1; i < points.length; i++) {
        pieceShape.lineTo(points [i][0], points [i][1]);
      }
      pieceShape.closePath();
      if (unrotatedBoundingRectangle == null) {
        unrotatedBoundingRectangle = pieceShape.createTransformedShape(rotation).getBounds2D();
      } else {
        unrotatedBoundingRectangle.add(pieceShape.createTransformedShape(rotation).getBounds2D());
      }
    }
    // Search center of the group
    Point2D center = new Point2D.Float((float)unrotatedBoundingRectangle.getCenterX(), (float)unrotatedBoundingRectangle.getCenterY());
    rotation.setToRotation(firstPiece.getAngle());
    rotation.transform(center, center);
    
    float elevation = Float.MAX_VALUE;
    float height    = 0;
    boolean movable = true;
    this.resizable = true;
    this.deformable = true;
    this.texturable = true;
    this.doorOrWindow = true;
    boolean visible = false;
    boolean modelMirrored = true;
    this.currency = firstPiece.getCurrency();
    // Search the lowest level elevation among grouped furniture
    Level minLevel = null;
    for (HomePieceOfFurniture piece : furniture) {
      Level level = piece.getLevel();
      if (level != null 
          && (minLevel == null
              || level.getElevation() < minLevel.getElevation())) {
        minLevel = level;
      }
    }
    for (HomePieceOfFurniture piece : furniture) {
      Level level = piece.getLevel();
      if (level != null) {
        elevation = Math.min(elevation, piece.getGroundElevation() - minLevel.getElevation());
      } else {
        elevation = Math.min(elevation, piece.getElevation());
      }
    }
    for (HomePieceOfFurniture piece : furniture) {
      if (piece.getLevel() != null) {
        piece.setElevation(piece.getGroundElevation() - minLevel.getElevation());
      }
      piece.setLevel(null);
      height = Math.max(height, piece.getElevation() + piece.getHeight());
      movable &= piece.isMovable();
      this.resizable &= piece.isResizable();
      this.deformable &= piece.isDeformable();
      this.texturable &= piece.isTexturable();
      this.doorOrWindow &= piece.isDoorOrWindow();
      visible |= piece.isVisible();
      modelMirrored &= piece.isModelMirrored();

      if (this.currency != null) {
        if (piece.getCurrency() == null
            || !piece.getCurrency().equals(this.currency)) {
          this.currency = null; 
        }
      }
    }
    if (this.resizable) {
      super.setWidth((float)unrotatedBoundingRectangle.getWidth());
      super.setDepth((float)unrotatedBoundingRectangle.getHeight());
      super.setHeight(height - elevation);
      super.setModelMirrored(modelMirrored);
    } else {
      this.fixedWidth = (float)unrotatedBoundingRectangle.getWidth();
      this.fixedDepth = (float)unrotatedBoundingRectangle.getHeight();
      this.fixedHeight = height - elevation;
    }
    setName(name);
    setNameVisible(false);
    setNameXOffset(0);
    setNameYOffset(0);
    setNameStyle(null);
    setDescription(null);
    setMovable(movable);
    setVisible(visible);
    if (this.texturable) {
      super.setColor(null);
      super.setTexture(null);
    }
    super.setX((float)center.getX());
    super.setY((float)center.getY());
    super.setAngle(firstPiece.getAngle());
    super.setElevation(elevation);
  }

  /**
   * Initializes new piece fields to their default values 
   * and reads piece from <code>in</code> stream with default reading method.
   */
  private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
    this.deformable = true;
    this.texturable = true;
    in.defaultReadObject();
  }

  /**
   * Returns all the pieces of the given <code>furniture</code> list.  
   */
  private List<HomePieceOfFurniture> getFurnitureWithoutGroups(List<HomePieceOfFurniture> furniture) {
    List<HomePieceOfFurniture> pieces = new ArrayList<HomePieceOfFurniture>();
    for (HomePieceOfFurniture piece : furniture) {
      if (piece instanceof HomeFurnitureGroup) {
        pieces.addAll(getFurnitureWithoutGroups(((HomeFurnitureGroup)piece).getFurniture()));
      } else {
        pieces.add(piece);
      }
    }
    return pieces;
  }
  
  /**
   * Returns an unmodifiable list of the furniture of this group.
   */
  public List<HomePieceOfFurniture> getFurniture() {
    return this.furniture;
  }
  
  /**
   * Returns <code>null</code>.
   */
  @Override
  public String getCatalogId() {
    return null;
  }

  /**
   * Returns <code>true</code> if all furniture of this group are movable.
   */
  @Override
  public boolean isMovable() {
    return super.isMovable();
  }
  
  /**
   * Sets whether this piece is movable or not.
   * @since 3.1
   */
  @Override
  public void setMovable(boolean movable) {
    super.setMovable(movable);
    for (HomePieceOfFurniture piece : this.furniture) {
      piece.setMovable(movable);
    }
  }

  /**
   * Returns <code>true</code> if all furniture of this group are doors or windows.
   */
  @Override
  public boolean isDoorOrWindow() {
    return this.doorOrWindow;
  }

  /**
   * Returns <code>true</code> if all furniture of this group are resizable.
   */
  @Override
  public boolean isResizable() {
    return this.resizable;
  }
  
  /**
   * Returns <code>true</code> if all furniture of this group are deformable.
   * @since 3.0
   */
  @Override
  public boolean isDeformable() {
    return this.deformable;
  }
  
  /**
   * Returns <code>true</code> if all furniture of this group are texturable.
   * @since 3.5
   */
  @Override
  public boolean isTexturable() {
    return this.texturable;
  }
  
  /**
   * Returns the width of this group.
   */
  @Override
  public float getWidth() {
    if (!this.resizable) {
      return this.fixedWidth;
    } else {
      return super.getWidth();
    }
  }
  
  /**
   * Returns the depth of this group.
   */
  @Override
  public float getDepth() {
    if (!this.resizable) {
      return this.fixedDepth;
    } else {
      return super.getDepth();
    }
  }
  
  /**
   * Returns the height of this group.
   */
  @Override
  public float getHeight() {
    if (!this.resizable) {
      return this.fixedHeight;
    } else {
      return super.getHeight();
    }
  }
  
  /**
   * Returns <code>null</code>.
   */
  public Content getIcon() {
    return null;
  }

  /**
   * Returns <code>null</code>.
   */
  public Content getPlanIcon() {
    return null;
  }

  /**
   * Returns <code>null</code>.
   */
  public Content getModel() {
    return null;
  }
  
  /**
   * Returns an identity matrix.
   */
  @Override
  public float [][] getModelRotation() {
    return IDENTITY;
  }
  
  /**
   * Returns <code>null</code>.
   * @since 3.5
   */
  @Override
  public String getStaircaseCutOutShape() {
    return null;
  }
  
  /**
   * Returns the price of the furniture of this group with a price.
   */
  @Override
  public BigDecimal getPrice() {
    BigDecimal price = null;
    for (HomePieceOfFurniture piece : this.furniture) {
      if (piece.getPrice() != null) {
        if (price == null) {
          price = piece.getPrice();
        } else {
          price = price.add(piece.getPrice()); 
        }
      }
    }
    if (price == null) {
      return super.getPrice();
    } else {
      return price;
    }
  }

  /**
   * Sets the price of this group.
   * @throws UnsupportedOperationException if the price of one of the pieces is set
   * @since 4.0
   */
  @Override
  public void setPrice(BigDecimal price) {
    for (HomePieceOfFurniture piece : this.furniture) {
      if (piece.getPrice() != null) {
        throw new UnsupportedOperationException("Can't change the price of a group containing pieces with a price");
      }
    }
    super.setPrice(price);
  }
  
  /**
   * Returns the VAT percentage of the furniture of this group 
   * or <code>null</code> if one piece has no VAT percentage 
   * or has a VAT percentage different from the other furniture.
   */
  @Override
  public BigDecimal getValueAddedTaxPercentage() {
    BigDecimal valueAddedTaxPercentage = this.furniture.get(0).getValueAddedTaxPercentage();
    if (valueAddedTaxPercentage != null) {
      for (HomePieceOfFurniture piece : this.furniture) {
        BigDecimal pieceValueAddedTaxPercentage = piece.getValueAddedTaxPercentage();
        if (pieceValueAddedTaxPercentage == null
            || !pieceValueAddedTaxPercentage.equals(valueAddedTaxPercentage)) {
          return null; 
        }
      }
    }
    return valueAddedTaxPercentage;
  }
  
  /**
   * Returns the currency of the furniture of this group 
   * or <code>null</code> if one piece has no currency 
   * or has a currency different from the other furniture.
   * @since 3.5
   */
  @Override
  public String getCurrency() {
    return this.currency;
  }
  
  /**
   * Returns the VAT of the furniture of this group.
   */
  @Override
  public BigDecimal getValueAddedTax() {
    BigDecimal valueAddedTax = null;
    for (HomePieceOfFurniture piece : furniture) {
      BigDecimal pieceValueAddedTax = piece.getValueAddedTax();
      if (pieceValueAddedTax != null) {
        if (valueAddedTax == null) {
          valueAddedTax = pieceValueAddedTax;
        } else {
          valueAddedTax = valueAddedTax.add(pieceValueAddedTax);
        }
      }
    }
    return valueAddedTax;
  }
  
  /**
   * Returns the total price of the furniture of this group.
   */
  @Override
  public BigDecimal getPriceValueAddedTaxIncluded() {
    BigDecimal priceValueAddedTaxIncluded = null;
    for (HomePieceOfFurniture piece : furniture) {
      if (piece.getPrice() != null) {
        if (priceValueAddedTaxIncluded == null) {
          priceValueAddedTaxIncluded = piece.getPriceValueAddedTaxIncluded();
        } else {
          priceValueAddedTaxIncluded = priceValueAddedTaxIncluded.add(piece.getPriceValueAddedTaxIncluded());
        }
      }
    }
    return priceValueAddedTaxIncluded;
  }
  
  /**
   * Returns <code>false</code>.
   */
  @Override
  public boolean isBackFaceShown() {
    return false;
  }
  
  /**
   * Sets the <code>color</code> of the furniture of this group.
   */
  @Override
  public void setColor(Integer color) {
    super.setColor(color);
    if (color != null) {      
      storeDefaultColorsAndTextures();
      for (HomePieceOfFurniture piece : this.furniture) {
        piece.setTexture(null);
        piece.setColor(color);
      } 
    } else if (getTexture() == null) {
      restoreDefaultColorsAndTextures();
    }
  }

  /**
   * Sets the <code>texture</code> of the furniture of this group.
   */
  @Override
  public void setTexture(HomeTexture texture) {
    super.setTexture(texture);
    if (texture != null) {      
      storeDefaultColorsAndTextures();
      for (HomePieceOfFurniture piece : this.furniture) {
        piece.setColor(null);
        piece.setTexture(texture);
      } 
    } else if (getColor() == null) {
      restoreDefaultColorsAndTextures();
    }
  }
  
  /**
   * Stores default colors and textures. 
   */
  private void storeDefaultColorsAndTextures() {
    if (this.furnitureDefaultColors == null) {
      // Retrieve default color and texture of child furniture
      Integer [] furnitureDefaultColors = new Integer [this.furniture.size()];
      HomeTexture [] furnitureDefaultTextures = new HomeTexture [this.furniture.size()];
      for (int i = 0; i < this.furniture.size(); i++) {
        furnitureDefaultColors [i] = this.furniture.get(i).getColor();
        furnitureDefaultTextures [i] = this.furniture.get(i).getTexture();
      } 
      this.furnitureDefaultColors = Arrays.asList(furnitureDefaultColors);
      this.furnitureDefaultTextures = Arrays.asList(furnitureDefaultTextures);
    }
  }
  
  /**
   * Restores default colors and textures
   */
  private void restoreDefaultColorsAndTextures() {
    if (this.furnitureDefaultColors != null) {
      for (int i = 0; i < this.furniture.size(); i++) {
        this.furniture.get(i).setColor(this.furnitureDefaultColors.get(i));
        this.furniture.get(i).setTexture(this.furnitureDefaultTextures.get(i));
      }
      this.furnitureDefaultColors = null;
      this.furnitureDefaultTextures = null;
    }
  }

  /**
   * Sets the <code>angle</code> of the furniture of this group.
   */
  @Override
  public void setAngle(float angle) {
    if (angle != getAngle()) {
      float angleDelta = angle - getAngle();
      super.setAngle(angle);
      double cosAngleDelta = Math.cos(angleDelta);
      double sinAngleDelta = Math.sin(angleDelta);
      for (HomePieceOfFurniture piece : this.furniture) {
        piece.setAngle(piece.getAngle() + angleDelta);     
        float newX = getX() + (float)((piece.getX() - getX()) * cosAngleDelta - (piece.getY() - getY()) * sinAngleDelta);
        float newY = getY() + (float)((piece.getX() - getX()) * sinAngleDelta + (piece.getY() - getY()) * cosAngleDelta);
        piece.setX(newX);
        piece.setY(newY);
      }
    }
  }
  
  /**
   * Sets the <code>abscissa</code> of this group and moves its furniture accordingly.
   */
  @Override
  public void setX(float x) {
    if (x != getX()) {
      float dx = x - getX();
      super.setX(x);
      for (HomePieceOfFurniture piece : this.furniture) {
        piece.setX(piece.getX() + dx);
      }
    }
  }

  /**
   * Sets the <code>ordinate</code> of this group and moves its furniture accordingly.
   */
  @Override
  public void setY(float y) {
    if (y != getY()) {
      float dy = y - getY();
      super.setY(y);
      for (HomePieceOfFurniture piece : this.furniture) {
        piece.setY(piece.getY() + dy);
      }
    }
  }

  /**
   * Sets the <code>width</code> of this group, then moves and resizes its furniture accordingly.
   */
  @Override
  public void setWidth(float width) {
    if (width != getWidth()) {
      float widthFactor = width / getWidth();
      super.setWidth(width);
      float angle = getAngle();
      for (HomePieceOfFurniture piece : this.furniture) {
        float angleDelta = piece.getAngle() - angle;
        float pieceWidth = piece.getWidth();
        float pieceDepth = piece.getDepth();
        piece.setWidth(pieceWidth + pieceWidth * (widthFactor - 1) * Math.abs((float)Math.cos(angleDelta)));
        piece.setDepth(pieceDepth + pieceDepth * (widthFactor - 1) * Math.abs((float)Math.sin(angleDelta)));
        // Rotate piece to angle 0
        double cosAngle = Math.cos(angle);
        double sinAngle = Math.sin(angle);
        float newX = getX() + (float)((piece.getX() - getX()) * cosAngle + (piece.getY() - getY()) * sinAngle);
        float newY = getY() + (float)((piece.getX() - getX()) * -sinAngle + (piece.getY() - getY()) * cosAngle);
        // Update its abscissa
        newX = getX() + (newX - getX()) * widthFactor; 
        // Rotate piece back to its angle
        piece.setX(getX() + (float)((newX - getX()) * cosAngle - (newY - getY()) * sinAngle));
        piece.setY(getY() + (float)((newX - getX()) * sinAngle + (newY - getY()) * cosAngle));
      }
    }
  }

  /**
   * Sets the <code>depth</code> of this group, then moves and resizes its furniture accordingly.
   */
  @Override
  public void setDepth(float depth) {
    if (depth != getDepth()) {
      float depthFactor = depth / getDepth();
      super.setDepth(depth);
      float angle = getAngle();
      for (HomePieceOfFurniture piece : this.furniture) {
        float angleDelta = piece.getAngle() - angle;
        float pieceWidth = piece.getWidth();
        float pieceDepth = piece.getDepth();
        piece.setWidth(pieceWidth + pieceWidth * (depthFactor - 1) * Math.abs((float)Math.sin(angleDelta)));
        piece.setDepth(pieceDepth + pieceDepth * (depthFactor - 1) * Math.abs((float)Math.cos(angleDelta)));
        // Rotate piece to angle 0
        double cosAngle = Math.cos(angle);
        double sinAngle = Math.sin(angle);
        float newX = getX() + (float)((piece.getX() - getX()) * cosAngle + (piece.getY() - getY()) * sinAngle);
        float newY = getY() + (float)((piece.getX() - getX()) * -sinAngle + (piece.getY() - getY()) * cosAngle);
        // Update its ordinate
        newY = getY() + (newY - getY()) * depthFactor;
        // Rotate piece back to its angle
        piece.setX(getX() + (float)((newX - getX()) * cosAngle - (newY - getY()) * sinAngle));
        piece.setY(getY() + (float)((newX - getX()) * sinAngle + (newY - getY()) * cosAngle));
      }
    }
  }

  /**
   * Sets the <code>height</code> of this group, then moves and resizes its furniture accordingly.
   */
  @Override
  public void setHeight(float height) {
    if (height != getHeight()) {
      float heightFactor = height / getHeight();
      super.setHeight(height);
      for (HomePieceOfFurniture piece : this.furniture) {
        piece.setHeight(piece.getHeight() * heightFactor);
        piece.setElevation(getElevation() 
            + (piece.getElevation() - getElevation()) * heightFactor);
      }
    }
  }

  /**
   * Sets the <code>elevation</code> of this group, then moves its furniture accordingly.
   */
  @Override
  public void setElevation(float elevation) {
    if (elevation != getElevation()) {
      float elevationDelta = elevation - getElevation();
      super.setElevation(elevation);
      for (HomePieceOfFurniture piece : this.furniture) {
        piece.setElevation(piece.getElevation() + elevationDelta);
      }
    }
  }
  
  /**
   * Sets whether the furniture of this group should be mirrored or not.
   */
  @Override
  public void setModelMirrored(boolean modelMirrored) {
    if (modelMirrored != isModelMirrored()) {
      super.setModelMirrored(modelMirrored);
      float angle = getAngle();
      for (HomePieceOfFurniture piece : this.furniture) {
        piece.setModelMirrored(!piece.isModelMirrored());
        // Rotate piece to angle 0
        double cosAngle = Math.cos(angle);
        double sinAngle = Math.sin(angle);
        float newX = getX() + (float)((piece.getX() - getX()) * cosAngle + (piece.getY() - getY()) * sinAngle);
        float newY = getY() + (float)((piece.getX() - getX()) * -sinAngle + (piece.getY() - getY()) * cosAngle);
        // Update its abscissa
        newX = getX() - (newX - getX()); 
        // Rotate piece back to its angle
        piece.setX(getX() + (float)((newX - getX()) * cosAngle - (newY - getY()) * sinAngle));
        piece.setY(getY() + (float)((newX - getX()) * sinAngle + (newY - getY()) * cosAngle));
      }
    }
  }
  
  /**
   * Sets whether the furniture of this group should be visible or not.
   */
  @Override
  public void setVisible(boolean visible) {
    super.setVisible(visible);
    for (HomePieceOfFurniture piece : this.furniture) {
      piece.setVisible(visible);
    }
  }

  /**
   * Set the level of this group and the furniture it contains.
   */
  @Override
  public void setLevel(Level level) {
    super.setLevel(level);
    for (HomePieceOfFurniture piece : this.furniture) {
      piece.setLevel(level);
    }
  }
  
  /**
   * Returns <code>true</code> if one of the pieces of this group intersects
   * with the horizontal rectangle which opposite corners are at points
   * (<code>x0</code>, <code>y0</code>) and (<code>x1</code>, <code>y1</code>).
   * @since 3.5
   */
  @Override
  public boolean intersectsRectangle(float x0, float y0, 
                                     float x1, float y1) {
    for (HomePieceOfFurniture piece : this.furniture) {
      if (piece.intersectsRectangle(x0, y0, x1, y1)) {
        return true;
      }
    }
    return false;
  }
  
  /**
   * Returns <code>true</code> if one of the pieces of this group contains 
   * the point at (<code>x</code>, <code>y</code>)
   * with a given <code>margin</code>.
   * @since 3.5
   */
  @Override
  public boolean containsPoint(float x, float y, float margin) {
    for (HomePieceOfFurniture piece : this.furniture) {
      if (piece.containsPoint(x, y, margin)) {
        return true;
      }
    }
    return false;
  }
  
  /**
   * Returns a clone of this group with cloned furniture.
   */
  @Override
  public HomeFurnitureGroup clone() {
    HomeFurnitureGroup clone = (HomeFurnitureGroup)super.clone();
    // Deep clone furniture managed by this group
    clone.furniture = new ArrayList<HomePieceOfFurniture>(this.furniture.size());
    for (HomePieceOfFurniture piece : this.furniture) {
      clone.furniture.add(piece.clone());
    }
    clone.furniture = Collections.unmodifiableList(clone.furniture);
    if (this.furnitureDefaultColors != null)  {
      clone.furnitureDefaultColors = new ArrayList<Integer>(this.furnitureDefaultColors);
    }
    if (this.furnitureDefaultTextures != null) {
      clone.furnitureDefaultTextures = new ArrayList<HomeTexture>(this.furnitureDefaultTextures);
    }
    return clone;
  }
}

