/*
 * LevelController.java 27 oct 2011
 *
 * Sweet Home 3D, Copyright (c) 2011 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.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.Level;
import com.eteks.sweethome3d.model.UserPreferences;

/**
 * A MVC controller for home levels view.
 * @author Emmanuel Puybaret
 */
public class LevelController implements Controller {
  /**
   * The properties that may be edited by the view associated to this controller. 
   */
  public enum Property {NAME, ELEVATION, FLOOR_THICKNESS, HEIGHT, LEVELS, SELECT_LEVEL_INDEX}
  
  private final Home                  home;
  private final UserPreferences       preferences;
  private final ViewFactory           viewFactory;
  private final UndoableEditSupport   undoSupport;
  private final PropertyChangeSupport propertyChangeSupport;
  private DialogView                  homeLevelView;

  private String   name;
  private Float    elevation;
  private Float    floorThickness;
  private Float    height;
  private Level [] levels;
  private Integer  selectedLevelIndex;
  
  /**
   * Creates the controller of home levels view with undo support.
   */
  public LevelController(Home home, 
                         UserPreferences preferences, 
                         ViewFactory viewFactory, 
                         UndoableEditSupport undoSupport) {
    this.home = home;
    this.preferences = preferences;
    this.viewFactory = viewFactory;
    this.undoSupport = undoSupport;
    this.propertyChangeSupport = new PropertyChangeSupport(this);
    
    updateProperties();
  }

  /**
   * Returns the view associated with this controller.
   */
  public DialogView getView() {
    // Create view lazily only once it's needed
    if (this.homeLevelView == null) {
      this.homeLevelView = this.viewFactory.createLevelView(this.preferences, this); 
    }
    return this.homeLevelView;
  }
  
  /**
   * 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 level in the home edited by this controller.
   */
  protected void updateProperties() {
    Level selectedLevel = this.home.getSelectedLevel();
    if (selectedLevel == null) {
      setName(null); // Nothing to edit
      setElevation(null);
      setFloorThickness(null);
      setHeight(null);      
      setSelectedLevelIndex(null);
    } else {
      setName(selectedLevel.getName());
      setElevation(selectedLevel.getElevation());
      setFloorThickness(selectedLevel.getFloorThickness());
      setHeight(selectedLevel.getHeight());
      setSelectedLevelIndex(this.home.getLevels().indexOf(selectedLevel));
    }
    setLevels(duplicate(this.home.getLevels().toArray(new Level [0])));
  }  
  
  private Level [] duplicate(Level[] levels) {
    for (int i = 0; i < levels.length; i++) {
      levels [i] = levels [i].clone();
    }
    return levels;
  }

  /**
   * Returns <code>true</code> if the given <code>property</code> is editable.
   * Depending on whether a property is editable or not, the view associated to this controller
   * may render it differently.
   * The implementation of this method always returns <code>true</code> except 
   * for <code>FLOOR_THICKNESS</code> if the selected level is the first level
   * and <code>HEIGHT</code> if the selected level is the last level. 
   */
  public boolean isPropertyEditable(Property property) {
    switch (property) {
      case FLOOR_THICKNESS :
        List<Level> levels = this.home.getLevels();
        Level selectedLevel = this.home.getSelectedLevel();
        int selectedLevelIndex;
        return selectedLevel != null
            && (selectedLevel.getElevation() > 0
                || ((selectedLevelIndex = levels.indexOf(selectedLevel)) != 0
                    && levels.get(selectedLevelIndex - 1).getElevation() != selectedLevel.getElevation()));
      default :
        return true;
    }
  }
  
  /**
   * Sets the edited name.
   */
  public void setName(String name) {
    if (name != this.name) {
      String oldName = this.name;
      this.name = name;
      this.propertyChangeSupport.firePropertyChange(Property.NAME.name(), oldName, name);
    }
  }

  /**
   * Returns the edited name.
   */
  public String getName() {
    return this.name;
  }
  
  /**
   * Sets the edited elevation.
   */
  public void setElevation(Float elevation) {
    if (elevation != this.elevation) {
      Float oldElevation = this.elevation;
      this.elevation = elevation;
      this.propertyChangeSupport.firePropertyChange(Property.ELEVATION.name(), oldElevation, elevation);
    }
  }

  /**
   * Returns the edited elevation.
   */
  public Float getElevation() {
    return this.elevation;
  }
  
  /**
   * Sets the edited floor thickness.
   */
  public void setFloorThickness(Float floorThickness) {
    if (floorThickness != this.floorThickness) {
      Float oldFloorThickness = this.floorThickness;
      this.floorThickness = floorThickness;
      this.propertyChangeSupport.firePropertyChange(Property.FLOOR_THICKNESS.name(), oldFloorThickness, floorThickness);
    }
  }

  /**
   * Returns the edited floor thickness.
   */
  public Float getFloorThickness() {
    return this.floorThickness;
  }
  
  /**
   * Sets the edited height.
   */
  public void setHeight(Float height) {
    if (height != this.height) {
      Float oldHeight = this.height;
      this.height = height;
      this.propertyChangeSupport.firePropertyChange(Property.HEIGHT.name(), oldHeight, height);
    }
  }

  /**
   * Returns the edited height.
   */
  public Float getHeight() {
    return this.height;
  }
  
  /**
   * Sets home levels.
   */
  private void setLevels(Level [] levels) {
    if (levels != this.levels) {
      Level [] oldLevels = this.levels;
      this.levels = levels;
      this.propertyChangeSupport.firePropertyChange(Property.LEVELS.name(), oldLevels, levels);
    }
  }

  /**
   * Returns a copy of home levels.
   */
  public Level [] getLevels() {
    return this.levels.clone();
  }
  
  /**
   * Sets the selected level index.
   */
  private void setSelectedLevelIndex(Integer selectedLevelIndex) {
    if (selectedLevelIndex != this.selectedLevelIndex) {
      Integer oldSelectedLevelIndex = this.selectedLevelIndex;
      this.selectedLevelIndex = selectedLevelIndex;
      this.propertyChangeSupport.firePropertyChange(Property.SELECT_LEVEL_INDEX.name(), oldSelectedLevelIndex, selectedLevelIndex);
    }
  }

  /**
   * Returns the selected level index.
   */
  public Integer getSelectedLevelIndex() {
    return this.selectedLevelIndex;
  }
  
  /**
   * Controls the modification of selected level in the edited home.
   */
  public void modifyLevels() {
    Level selectedLevel = this.home.getSelectedLevel();
    if (selectedLevel != null) {
      String name = getName();
      Float elevation = getElevation();
      Float floorThickness = getFloorThickness();
      Float height = getHeight();
      
      ModifiedLevel modifiedLevel = new ModifiedLevel(selectedLevel);
      // Apply modification
      doModifyLevel(modifiedLevel, name, elevation, floorThickness, height);
      if (this.undoSupport != null) {
        UndoableEdit undoableEdit = new LevelModificationUndoableEdit(
            this.preferences, modifiedLevel, name, elevation, floorThickness, height);
        this.undoSupport.postEdit(undoableEdit);
      }
      if (name != null) {
        this.preferences.addAutoCompletionString("LevelName", name);
      }
    }
  }
  
  /**
   * Undoable edit for level modification. This class isn't anonymous to avoid
   * being bound to controller and its view.
   */
  private static class LevelModificationUndoableEdit extends AbstractUndoableEdit {
    private final UserPreferences  preferences;
    private final ModifiedLevel    modifiedLevel;
    private final String           name;
    private final Float            elevation;
    private final Float            floorThickness;
    private final Float            height;

    private LevelModificationUndoableEdit(UserPreferences preferences,
                                          ModifiedLevel modifiedLevel, 
                                          String name,
                                          Float elevation,
                                          Float floorThickness,
                                          Float height) {
      this.preferences = preferences;
      this.modifiedLevel = modifiedLevel;
      this.name = name;
      this.elevation = elevation;
      this.floorThickness = floorThickness;
      this.height = height;
    }

    @Override
    public void undo() throws CannotUndoException {
      super.undo();
      undoModifyLevel(this.modifiedLevel);
    }

    @Override
    public void redo() throws CannotRedoException {
      super.redo();
      doModifyLevel(this.modifiedLevel, this.name, this.elevation, this.floorThickness, this.height); 
    }

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

  /**
   * Modifies level properties with the values in parameter.
   */
  private static void doModifyLevel(ModifiedLevel modifiedLevel, 
                                    String name, Float elevation, 
                                    Float floorThickness, Float height) {
    Level level = modifiedLevel.getLevel();
    if (name != null) {
      level.setName(name);
    }
    if (elevation != null) {
      level.setElevation(elevation); 
    }
    if (floorThickness != null) {
      level.setFloorThickness(floorThickness);
    }
    if (height != null) {
      level.setHeight(height);
    }
  }

  /**
   * Restores level properties from the values stored in <code>modifiedLevel</code>.
   */
  private static void undoModifyLevel(ModifiedLevel modifiedLevel) {
    modifiedLevel.reset();
  }

  /**
   * Stores the current properties values of a modified level.
   */
  private static class ModifiedLevel {
    private final Level   level;
    private final String  name;
    private final float   elevation;
    private final float   floorThickness;
    private final float   height;

    public ModifiedLevel(Level level) {
      this.level = level;
      this.name = level.getName();
      this.elevation = level.getElevation();
      this.floorThickness = level.getFloorThickness();
      this.height = level.getHeight();
    }

    public Level getLevel() {
      return this.level;
    }
        
    public void reset() {
      this.level.setName(this.name);
      this.level.setElevation(this.elevation);
      this.level.setFloorThickness(this.floorThickness);
      this.level.setHeight(this.height);
    }
  }
}
