/*
 * PolylinePanel.java 
 *
 * Copyright (c) 2009 Plan PHP All Rights Reserved.
 */
package com.eteks.sweethome3d.swing;

import java.awt.BasicStroke;
import java.awt.Component;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.awt.RenderingHints;
import java.awt.Shape;
import java.awt.event.ItemEvent;
import java.awt.event.ItemListener;
import java.awt.geom.Arc2D;
import java.awt.geom.GeneralPath;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import javax.swing.DefaultComboBoxModel;
import javax.swing.DefaultListCellRenderer;
import javax.swing.Icon;
import javax.swing.JComboBox;
import javax.swing.JComponent;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JSpinner;
import javax.swing.KeyStroke;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;

import com.eteks.sweethome3d.model.Polyline;
import com.eteks.sweethome3d.model.Polyline.ArrowStyle;
import com.eteks.sweethome3d.model.UserPreferences;
import com.eteks.sweethome3d.tools.OperatingSystem;
import com.eteks.sweethome3d.viewcontroller.DialogView;
import com.eteks.sweethome3d.viewcontroller.PolylineController;
import com.eteks.sweethome3d.viewcontroller.View;

/**
 * User preferences panel.
 * @author Emmanuel Puybaret
 */
public class PolylinePanel extends JPanel implements DialogView {
  private final PolylineController controller;
  private JLabel         thicknessLabel;
  private JSpinner       thicknessSpinner;
  private JLabel         arrowsStyleLabel;
  private JComboBox      arrowsStyleComboBox;
  private JLabel         joinStyleLabel;
  private JComboBox      joinStyleComboBox;
  private JLabel         dashStyleLabel;
  private JComboBox      dashStyleComboBox;
  private JLabel         colorLabel;
  private ColorButton    colorButton;
  private String         dialogTitle;
  
  /**
   * Creates a preferences panel that layouts the editable properties
   * of its <code>controller</code>. 
   */
  public PolylinePanel(UserPreferences preferences,
                              PolylineController controller) {
    super(new GridBagLayout());
    this.controller = controller;
    createComponents(preferences, controller);
    setMnemonics(preferences);
    layoutComponents();
  }
  
  /**
   * Creates and initializes components and spinners model.
   */
  private void createComponents(UserPreferences preferences,
                                final PolylineController controller) {
    // Create thickness label and spinner bound to controller THICKNESS property
    this.thicknessLabel = new JLabel(SwingTools.getLocalizedLabelText(preferences, 
        PolylinePanel.class, "thicknessLabel.text", preferences.getLengthUnit().getName()));
    final NullableSpinner.NullableSpinnerLengthModel thicknessSpinnerModel = 
        new NullableSpinner.NullableSpinnerLengthModel(preferences, preferences.getLengthUnit().getMinimumLength(), 20f);
    this.thicknessSpinner = new NullableSpinner(thicknessSpinnerModel);
    thicknessSpinnerModel.setNullable(controller.getThickness() == null);
    thicknessSpinnerModel.setLength(controller.getThickness());
    thicknessSpinnerModel.addChangeListener(new ChangeListener() {
        public void stateChanged(ChangeEvent ev) {
          controller.setThickness(thicknessSpinnerModel.getLength());
        }
      });
    controller.addPropertyChangeListener(PolylineController.Property.THICKNESS, 
        new PropertyChangeListener() {
          public void propertyChange(PropertyChangeEvent ev) {
            thicknessSpinnerModel.setLength(controller.getThickness());
          }
        });

    // Create cap style label and combo box bound to controller CAP_STYLE property
    this.arrowsStyleLabel = new JLabel(SwingTools.getLocalizedLabelText(preferences, 
        PolylinePanel.class, "arrowsStyleLabel.text"));    
    ArrowsStyle[] arrowsStyles = ArrowsStyle.getArrowsStyle();
    if (controller.getCapStyle() == null) {
      List<ArrowsStyle> arrowsStylesList = new ArrayList<ArrowsStyle>();
      arrowsStylesList.add(null);
      arrowsStylesList.addAll(Arrays.asList(arrowsStyles));
      arrowsStyles = arrowsStylesList.toArray(new ArrowsStyle [arrowsStylesList.size()]);
    }
    this.arrowsStyleComboBox = new JComboBox(new DefaultComboBoxModel(arrowsStyles));
    this.arrowsStyleComboBox.setMaximumRowCount(arrowsStyles.length);
    this.arrowsStyleComboBox.setRenderer(new DefaultListCellRenderer() {
        @Override
        public Component getListCellRendererComponent(final JList list, 
            Object value, int index, boolean isSelected, boolean cellHasFocus) {
          final ArrowsStyle arrowsStyle = (ArrowsStyle)value;
          final Component component = super.getListCellRendererComponent(
              list, "", index, isSelected, cellHasFocus);
          setIcon(new Icon() {
              public int getIconWidth() {
                return 64;
              }
        
              public int getIconHeight() {
                return 16;
              }
        
              public void paintIcon(Component c, Graphics g, int x, int y) {
                if (arrowsStyle != null) {
                  Graphics2D g2D = (Graphics2D)g;
                  if (OperatingSystem.isMacOSXLeopardOrSuperior()) {
                    g2D.translate(0, 2);
                  }
                  g2D.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
                  g2D.setColor(list.getForeground());
                  int iconWidth = getIconWidth();
                  g2D.setStroke(new BasicStroke(2));
                  g2D.drawLine(6, 8, iconWidth - 6, 8);
                  switch (arrowsStyle.getStartArrowStyle()) {
                    case NONE :
                      break;
                    case DISC : 
                      g2D.fillOval(4, 4, 9, 9);
                      break;
                    case OPEN :
                      g2D.drawPolyline(new int [] {15, 5, 15}, new int [] {4, 8, 12}, 3);
                      break;
                    case DELTA :
                      g2D.fillPolygon(new int [] {3, 15, 15}, new int [] {8, 3, 13}, 3);
                      break;
                  }
                  switch (arrowsStyle.getEndArrowStyle()) {
                    case NONE :
                      break;
                    case DISC : 
                      g2D.fillOval(iconWidth - 12, 4, 9, 9);
                      break;
                    case OPEN :
                      g2D.drawPolyline(new int [] {iconWidth - 14, iconWidth - 4, iconWidth - 14}, new int [] {4, 8, 12}, 3);
                      break;
                    case DELTA :
                      g2D.fillPolygon(new int [] {iconWidth - 2, iconWidth - 14, iconWidth - 14}, new int [] {8, 3, 13}, 3);
                      break;
                  }
                }
              }
            });
          return component;
        }
      });
    this.arrowsStyleComboBox.addItemListener(new ItemListener() {
      public void itemStateChanged(ItemEvent ev) {
          ArrowsStyle arrowsStyle = (ArrowsStyle)arrowsStyleComboBox.getSelectedItem();
          if (arrowsStyle != null) {
            controller.setStartArrowStyle(arrowsStyle.getStartArrowStyle());
            controller.setEndArrowStyle(arrowsStyle.getEndArrowStyle());
          } else {
            controller.setStartArrowStyle(null);
            controller.setEndArrowStyle(null);
          }
        }
      });
    PropertyChangeListener arrowStyleChangeListener = new PropertyChangeListener() {
        public void propertyChange(PropertyChangeEvent ev) {
          ArrowStyle startArrowStyle = controller.getStartArrowStyle();
          ArrowStyle endArrowStyle = controller.getEndArrowStyle();
          if (startArrowStyle != null && endArrowStyle != null) {
            arrowsStyleComboBox.setSelectedItem(new ArrowsStyle(startArrowStyle, endArrowStyle));
          } else {
            arrowsStyleComboBox.setSelectedItem(null);
          }
          arrowsStyleLabel.setEnabled(controller.isArrowsStyleEditable());
          arrowsStyleComboBox.setEnabled(controller.isArrowsStyleEditable());
        }
      };
    controller.addPropertyChangeListener(PolylineController.Property.START_ARROW_STYLE, 
        arrowStyleChangeListener);
    controller.addPropertyChangeListener(PolylineController.Property.END_ARROW_STYLE, 
        arrowStyleChangeListener);
    arrowStyleChangeListener.propertyChange(null);
    
    // Create join style label and combo box bound to controller JOIN_STYLE property
    this.joinStyleLabel = new JLabel(SwingTools.getLocalizedLabelText(preferences, 
        PolylinePanel.class, "joinStyleLabel.text"));    
    Polyline.JoinStyle [] joinStyles = Polyline.JoinStyle.values();
    if (controller.getJoinStyle() == null) {
      List<Polyline.JoinStyle> joinStylesList = new ArrayList<Polyline.JoinStyle>();
      joinStylesList.add(null);
      joinStylesList.addAll(Arrays.asList(joinStyles));
      joinStyles = joinStylesList.toArray(new Polyline.JoinStyle [joinStylesList.size()]);
    }
    this.joinStyleComboBox = new JComboBox(new DefaultComboBoxModel(joinStyles));
    final GeneralPath joinPath = new GeneralPath();
    joinPath.moveTo(4, 4);
    joinPath.lineTo(58, 4);
    joinPath.lineTo(36, 14);
    final Shape curvedPath = new Arc2D.Float(-7, 6, 80, 40, 47, 86, Arc2D.OPEN);
    this.joinStyleComboBox.setRenderer(new DefaultListCellRenderer() {
        @Override
        public Component getListCellRendererComponent(final JList list, 
            Object value, int index, boolean isSelected, boolean cellHasFocus) {
          final Polyline.JoinStyle joinStyle = (Polyline.JoinStyle)value;
          final Component component = super.getListCellRendererComponent(
              list, "", index, isSelected, cellHasFocus);
          setIcon(new Icon() {
              public int getIconWidth() {
                return 64;
              }
        
              public int getIconHeight() {
                return 16;
              }
        
              public void paintIcon(Component c, Graphics g, int x, int y) {
                if (joinStyle != null) {
                  Graphics2D g2D = (Graphics2D)g;
                  if (OperatingSystem.isMacOSXLeopardOrSuperior()) {
                    g2D.translate(0, 2);
                  }
                  g2D.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
                  g2D.setColor(list.getForeground());
                  g2D.setStroke(SwingTools.getStroke(6, Polyline.CapStyle.BUTT, joinStyle, Polyline.DashStyle.SOLID));
                  if (joinStyle == Polyline.JoinStyle.CURVED) {
                    g2D.draw(curvedPath);
                  } else {
                    g2D.draw(joinPath);
                  }
                }
              }
            });
          return component;
        }
      });
    this.joinStyleComboBox.addItemListener(new ItemListener() {
      public void itemStateChanged(ItemEvent ev) {
          controller.setJoinStyle((Polyline.JoinStyle)joinStyleComboBox.getSelectedItem());
        }
      });
    PropertyChangeListener joinStyleChangeListener = new PropertyChangeListener() {
        public void propertyChange(PropertyChangeEvent ev) {
          joinStyleLabel.setEnabled(controller.isJoinStyleEditable());
          joinStyleComboBox.setEnabled(controller.isJoinStyleEditable());
          joinStyleComboBox.setSelectedItem(controller.getJoinStyle());
        }
      };
    controller.addPropertyChangeListener(PolylineController.Property.JOIN_STYLE, 
        joinStyleChangeListener);
    joinStyleChangeListener.propertyChange(null);
    
    // Create dash style label and combo box bound to controller DASH_STYLE property
    this.dashStyleLabel = new JLabel(SwingTools.getLocalizedLabelText(preferences, 
        PolylinePanel.class, "dashStyleLabel.text"));    
    Polyline.DashStyle [] dashStyles = Polyline.DashStyle.values();
    if (controller.getDashStyle() == null) {
      List<Polyline.DashStyle> dashStylesList = new ArrayList<Polyline.DashStyle>();
      dashStylesList.add(null);
      dashStylesList.addAll(Arrays.asList(dashStyles));
      dashStyles = dashStylesList.toArray(new Polyline.DashStyle [dashStylesList.size()]);
    }
    this.dashStyleComboBox = new JComboBox(new DefaultComboBoxModel(dashStyles));
    this.dashStyleComboBox.setRenderer(new DefaultListCellRenderer() {
        @Override
        public Component getListCellRendererComponent(final JList list, 
            Object value, int index, boolean isSelected, boolean cellHasFocus) {
          final Polyline.DashStyle dashStyle = (Polyline.DashStyle)value;
          final Component component = super.getListCellRendererComponent(
              list, "", index, isSelected, cellHasFocus);
          setIcon(new Icon() {
              public int getIconWidth() {
                return 64;
              }
        
              public int getIconHeight() {
                return 16;
              }
        
              public void paintIcon(Component c, Graphics g, int x, int y) {
                if (dashStyle != null) {
                  Graphics2D g2D = (Graphics2D)g;
                  if (OperatingSystem.isMacOSXLeopardOrSuperior()) {
                    g2D.translate(0, 2);
                  }
                  g2D.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
                  g2D.setColor(list.getForeground());
                  g2D.setStroke(SwingTools.getStroke(2, Polyline.CapStyle.BUTT, Polyline.JoinStyle.MITER, dashStyle));
                  g2D.drawLine(4, 8, getIconWidth() - 4, 8);
                }
              }
            });
          return component;
        }
      });
    this.dashStyleComboBox.setSelectedItem(controller.getDashStyle());
    this.dashStyleComboBox.addItemListener(new ItemListener() {
      public void itemStateChanged(ItemEvent ev) {
          controller.setDashStyle((Polyline.DashStyle)dashStyleComboBox.getSelectedItem());
        }
      });
    controller.addPropertyChangeListener(PolylineController.Property.DASH_STYLE, 
        new PropertyChangeListener() {
          public void propertyChange(PropertyChangeEvent ev) {
            dashStyleComboBox.setSelectedItem(controller.getDashStyle());
          }
        });
    
    // Create color label and its button bound to COLOR controller property
    this.colorLabel = new JLabel(SwingTools.getLocalizedLabelText(preferences, 
        PolylinePanel.class, "colorLabel.text"));
    this.colorButton = new ColorButton(preferences);
    this.colorButton.setColorDialogTitle(preferences.getLocalizedString(
        PolylinePanel.class, "colorDialog.title"));
    this.colorButton.setColor(controller.getColor());
    this.colorButton.addPropertyChangeListener(ColorButton.COLOR_PROPERTY, 
        new PropertyChangeListener() {
          public void propertyChange(PropertyChangeEvent ev) {
            controller.setColor(colorButton.getColor());
          }
        });
    controller.addPropertyChangeListener(PolylineController.Property.COLOR, 
        new PropertyChangeListener() {
          public void propertyChange(PropertyChangeEvent ev) {
            colorButton.setColor(controller.getColor());
          }
        });


    this.dialogTitle = preferences.getLocalizedString(PolylinePanel.class, "polyline.title");
  }
  
  /**
   * Sets components mnemonics and label / component associations.
   */
  private void setMnemonics(UserPreferences preferences) {
    if (!OperatingSystem.isMacOSX()) {
      this.thicknessLabel.setDisplayedMnemonic(KeyStroke.getKeyStroke(preferences.getLocalizedString(
          PolylinePanel.class, "thicknessLabel.mnemonic")).getKeyCode());
      this.thicknessLabel.setLabelFor(this.thicknessSpinner);
      this.arrowsStyleLabel.setDisplayedMnemonic(KeyStroke.getKeyStroke(preferences.getLocalizedString(
          PolylinePanel.class, "arrowsStyleLabel.mnemonic")).getKeyCode());
      this.arrowsStyleLabel.setLabelFor(this.arrowsStyleComboBox);
      this.joinStyleLabel.setDisplayedMnemonic(KeyStroke.getKeyStroke(preferences.getLocalizedString(
          PolylinePanel.class, "joinStyleLabel.mnemonic")).getKeyCode());
      this.joinStyleLabel.setLabelFor(this.joinStyleComboBox);
      this.dashStyleLabel.setDisplayedMnemonic(KeyStroke.getKeyStroke(preferences.getLocalizedString(
          PolylinePanel.class, "dashStyleLabel.mnemonic")).getKeyCode());
      this.dashStyleLabel.setLabelFor(this.dashStyleComboBox);
      this.colorLabel.setDisplayedMnemonic(KeyStroke.getKeyStroke(preferences.getLocalizedString(
          PolylinePanel.class, "colorLabel.mnemonic")).getKeyCode());
      this.colorLabel.setLabelFor(this.colorButton);
    }
  }
  
  /**
   * Layouts panel components in panel with their labels. 
   */
  private void layoutComponents() {
    int labelAlignment = OperatingSystem.isMacOSX() 
        ? GridBagConstraints.LINE_END
        : GridBagConstraints.LINE_START;
    Insets labelInsets = new Insets(0, 0, 5, 5);
    // First row
    add(this.thicknessLabel, new GridBagConstraints(
        0, 0, 1, 1, 0, 0, labelAlignment, 
        GridBagConstraints.NONE, labelInsets, 0, 0));
    Insets rightComponentInsets = new Insets(0, 0, 5, 0);
    add(this.thicknessSpinner, new GridBagConstraints(
        1, 0, 1, 1, 0, 0, GridBagConstraints.LINE_START, 
        GridBagConstraints.HORIZONTAL, rightComponentInsets, 0, 0));
    // Second row
    add(this.arrowsStyleLabel, new GridBagConstraints(
        0, 1, 1, 1, 0, 0, labelAlignment, 
        GridBagConstraints.NONE, labelInsets, 0, 0));
    add(this.arrowsStyleComboBox, new GridBagConstraints(
        1, 1, 1, 1, 0, 0, GridBagConstraints.LINE_START, 
        GridBagConstraints.HORIZONTAL, rightComponentInsets, 0, 0));
    // Third row
    add(this.joinStyleLabel, new GridBagConstraints(
        0, 2, 1, 1, 0, 0, labelAlignment, 
        GridBagConstraints.NONE, labelInsets, 0, 0));
    add(this.joinStyleComboBox, new GridBagConstraints(
        1, 2, 1, 1, 0, 0, GridBagConstraints.LINE_START, 
        GridBagConstraints.HORIZONTAL, rightComponentInsets, 0, 0));
    // Fourth row
    add(this.dashStyleLabel, new GridBagConstraints(
        0, 3, 1, 1, 0, 0, labelAlignment, 
        GridBagConstraints.NONE, labelInsets, 0, 0));
    add(this.dashStyleComboBox, new GridBagConstraints(
        1, 3, 1, 1, 0, 0, GridBagConstraints.LINE_START, 
        GridBagConstraints.HORIZONTAL, rightComponentInsets, 0, 0));
    // Last row
    add(this.colorLabel, new GridBagConstraints(
        0, 4, 1, 1, 0, 0, labelAlignment, 
        GridBagConstraints.NONE, new Insets(0, 0, 0, 5), 0, 0));
    add(this.colorButton, new GridBagConstraints(
        1, 4, 1, 1, 0, 0, GridBagConstraints.LINE_START, 
        GridBagConstraints.HORIZONTAL, new Insets(0, OperatingSystem.isMacOSX() ? 2  : -1, 0, OperatingSystem.isMacOSX() ? 3  : -1), 0, 0));
  }

  /**
   * Displays this panel in a dialog box. 
   */
  public void displayView(View parentView) {
    if (SwingTools.showConfirmDialog((JComponent)parentView, this, this.dialogTitle, 
          ((JSpinner.DefaultEditor)this.thicknessSpinner.getEditor()).getTextField()) == JOptionPane.OK_OPTION
        && this.controller != null) {
      this.controller.modifyPolylines();
    }
  }
  
  /**
   * A tuple storing start and end arrow styles.
   * @author Emmanuel Puybaret
   */
  private static class ArrowsStyle {
    private static List<ArrowsStyle> arrowsStyle;
    private final Polyline.ArrowStyle startArrowStyle;
    private final Polyline.ArrowStyle endArrowStyle;
    
    public ArrowsStyle(ArrowStyle startArrowStyle, ArrowStyle endArrowStyle) {
      this.startArrowStyle = startArrowStyle;
      this.endArrowStyle = endArrowStyle;
    }
    
    public Polyline.ArrowStyle getStartArrowStyle() {
      return this.startArrowStyle;
    }
    
    public Polyline.ArrowStyle getEndArrowStyle() {
      return this.endArrowStyle;
    }
    
    
    
    @Override
    public int hashCode() {
      int hashCode = 0;
      if (this.startArrowStyle != null) {
        hashCode = this.startArrowStyle.hashCode();
      }
      if (this.endArrowStyle != null) {
        hashCode += this.endArrowStyle.hashCode();
      }
      return hashCode;
    }

    @Override
    public boolean equals(Object obj) {
      if (obj instanceof ArrowsStyle) {
        ArrowsStyle arrowsStyle = (ArrowsStyle)obj;
        return this.startArrowStyle == arrowsStyle.startArrowStyle
            && this.endArrowStyle == arrowsStyle.endArrowStyle;
      } else {
        return false;
      }
    }

    public static ArrowsStyle [] getArrowsStyle() {
      if (arrowsStyle == null) {
        ArrowStyle [] arrowStyles = Polyline.ArrowStyle.values();
        arrowsStyle = new ArrayList<ArrowsStyle>(arrowStyles.length * arrowStyles.length);
        for (ArrowStyle startArrowStyle : arrowStyles) {
          for (ArrowStyle endArrowStyle : arrowStyles) {
            arrowsStyle.add(new ArrowsStyle(startArrowStyle, endArrowStyle));
          }
        }
      }
      return arrowsStyle.toArray(new ArrowsStyle [arrowsStyle.size()]);
    }
  }
}
