/*
 * PhotoSizeAndQualityPanel.java 25 nov 2012
 *
 * Sweet Home 3D, Copyright (c) 2012 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.swing;

import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.ComponentOrientation;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.awt.event.ItemEvent;
import java.awt.event.ItemListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.IOException;
import java.lang.ref.WeakReference;
import java.util.Locale;

import javax.swing.DefaultListCellRenderer;
import javax.swing.JCheckBox;
import javax.swing.JComboBox;
import javax.swing.JComponent;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JPanel;
import javax.swing.JSlider;
import javax.swing.JSpinner;
import javax.swing.KeyStroke;
import javax.swing.SpinnerNumberModel;
import javax.swing.ToolTipManager;
import javax.swing.event.AncestorEvent;
import javax.swing.event.AncestorListener;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;

import com.eteks.sweethome3d.j3d.Component3DManager;
import com.eteks.sweethome3d.model.AspectRatio;
import com.eteks.sweethome3d.model.Home;
import com.eteks.sweethome3d.model.UserPreferences;
import com.eteks.sweethome3d.tools.OperatingSystem;
import com.eteks.sweethome3d.tools.ResourceURLContent;
import com.eteks.sweethome3d.viewcontroller.AbstractPhotoController;

/**
 * A panel to edit photo size and quality.
 * @author Emmanuel Puybaret
 */
public class PhotoSizeAndQualityPanel extends JPanel {
  private JLabel                        widthLabel;
  private JSpinner                      widthSpinner;
  private JLabel                        heightLabel;
  private JSpinner                      heightSpinner;
  private JCheckBox                     applyProportionsCheckBox;
  private JComboBox                     aspectRatioComboBox;
  private JLabel                        qualityLabel;
  private JSlider                       qualitySlider;
  private JLabel                        fastQualityLabel;
  private JLabel                        bestQualityLabel;

  public PhotoSizeAndQualityPanel(Home home,
                    UserPreferences preferences,
                    AbstractPhotoController controller) {
    super(new GridBagLayout());
    createComponents(home, preferences, controller);
    setMnemonics(preferences);
    layoutComponents();

    preferences.addPropertyChangeListener(UserPreferences.Property.LANGUAGE, new LanguageChangeListener(this));
  }

  /**
   * Creates and initializes components.
   */
  private void createComponents(final Home home,
                                final UserPreferences preferences,
                                final AbstractPhotoController controller) {
    // Create width label and spinner bound to WIDTH controller property
    this.widthLabel = new JLabel();
    final SpinnerNumberModel widthSpinnerModel = new SpinnerNumberModel(480, 10, 10000, 10);
    this.widthSpinner = new AutoCommitSpinner(widthSpinnerModel);
    widthSpinnerModel.setValue(controller.getWidth());
    widthSpinnerModel.addChangeListener(new ChangeListener() {
        public void stateChanged(ChangeEvent ev) {
          controller.setWidth(((Number)widthSpinnerModel.getValue()).intValue());
        }
      });
    controller.addPropertyChangeListener(AbstractPhotoController.Property.WIDTH,
        new PropertyChangeListener() {
          public void propertyChange(PropertyChangeEvent ev) {
            widthSpinnerModel.setValue(controller.getWidth());
          }
        });


    // Create height label and spinner bound to HEIGHT controller property
    this.heightLabel = new JLabel();
    final SpinnerNumberModel heightSpinnerModel = new SpinnerNumberModel(480, 10, 10000, 10);
    this.heightSpinner = new AutoCommitSpinner(heightSpinnerModel);
    heightSpinnerModel.setValue(controller.getHeight());
    heightSpinnerModel.addChangeListener(new ChangeListener() {
        public void stateChanged(ChangeEvent ev) {
          controller.setHeight(((Number)heightSpinnerModel.getValue()).intValue());
        }
      });
    controller.addPropertyChangeListener(AbstractPhotoController.Property.HEIGHT,
        new PropertyChangeListener() {
          public void propertyChange(PropertyChangeEvent ev) {
            heightSpinnerModel.setValue(controller.getHeight());
          }
        });

    // Create apply proportions check box bound to ASPECT_RATIO controller property
    boolean notFreeAspectRatio = controller.getAspectRatio() != AspectRatio.FREE_RATIO;
    this.applyProportionsCheckBox = new JCheckBox();
    this.applyProportionsCheckBox.setSelected(notFreeAspectRatio);
    this.applyProportionsCheckBox.addItemListener(new ItemListener() {
        public void itemStateChanged(ItemEvent ev) {
          controller.setAspectRatio(applyProportionsCheckBox.isSelected()
              ? (AspectRatio)aspectRatioComboBox.getSelectedItem()
              : AspectRatio.FREE_RATIO);
        }
      });
    this.aspectRatioComboBox = new JComboBox(new Object [] {
        AspectRatio.VIEW_3D_RATIO,
        AspectRatio.SQUARE_RATIO,
        AspectRatio.RATIO_4_3,
        AspectRatio.RATIO_3_2,
        AspectRatio.RATIO_16_9,
        AspectRatio.RATIO_2_1,
        AspectRatio.RATIO_24_10});
    this.aspectRatioComboBox.setRenderer(new DefaultListCellRenderer() {
        @Override
        public Component getListCellRendererComponent(JList list, Object value,
                                                      int index, boolean isSelected, boolean cellHasFocus) {
          AspectRatio aspectRatio = (AspectRatio)value;
          String displayedValue = "";
          if (aspectRatio != AspectRatio.FREE_RATIO) {
            switch (aspectRatio) {
              case VIEW_3D_RATIO :
                displayedValue = preferences.getLocalizedString(
                    PhotoSizeAndQualityPanel.class, "aspectRatioComboBox.view3DRatio.text");
                break;
              case SQUARE_RATIO :
                displayedValue = preferences.getLocalizedString(
                    PhotoSizeAndQualityPanel.class, "aspectRatioComboBox.squareRatio.text");
                break;
              case RATIO_4_3 :
                displayedValue = preferences.getLocalizedString(
                    PhotoSizeAndQualityPanel.class, "aspectRatioComboBox.4_3Ratio.text");
                break;
              case RATIO_3_2 :
                displayedValue = preferences.getLocalizedString(
                    PhotoSizeAndQualityPanel.class, "aspectRatioComboBox.3_2Ratio.text");
                break;
              case RATIO_16_9 :
                displayedValue = preferences.getLocalizedString(
                    PhotoSizeAndQualityPanel.class, "aspectRatioComboBox.16_9Ratio.text");
                break;
              case RATIO_2_1 :
                displayedValue = preferences.getLocalizedString(
                    PhotoSizeAndQualityPanel.class, "aspectRatioComboBox.2_1Ratio.text");
                break;
              case RATIO_24_10 :
                displayedValue = preferences.getLocalizedString(
                    PhotoSizeAndQualityPanel.class, "aspectRatioComboBox.2.40_1Ratio.text");
                break;
            }
          }
          return super.getListCellRendererComponent(list, displayedValue, index, isSelected,
              cellHasFocus);
        }
      });
    this.aspectRatioComboBox.addItemListener(new ItemListener() {
        public void itemStateChanged(ItemEvent ev) {
          controller.setAspectRatio((AspectRatio)aspectRatioComboBox.getSelectedItem());
        }
      });
    this.aspectRatioComboBox.setEnabled(notFreeAspectRatio);
    this.aspectRatioComboBox.setSelectedItem(controller.getAspectRatio());
    controller.addPropertyChangeListener(AbstractPhotoController.Property.ASPECT_RATIO,
        new PropertyChangeListener() {
          public void propertyChange(PropertyChangeEvent ev) {
            boolean notFreeAspectRatio = controller.getAspectRatio() != AspectRatio.FREE_RATIO;
            applyProportionsCheckBox.setSelected(notFreeAspectRatio);
            aspectRatioComboBox.setEnabled(notFreeAspectRatio);
            aspectRatioComboBox.setSelectedItem(controller.getAspectRatio());
          }
        });

    // Quality label and slider bound to QUALITY controller property
    this.qualityLabel = new JLabel();
    this.fastQualityLabel = new JLabel();
    this.bestQualityLabel = new JLabel();
    Dimension imageSize;
    try {
      imageSize = SwingTools.getImageSizeInPixels(new ResourceURLContent(PhotoSizeAndQualityPanel.class,
            "resources/quality0.jpg"));
    } catch (IOException ex) {
      // Shouldn't happen since resource exists
      imageSize = null;
    }
    float resolutionScale = SwingTools.getResolutionScale();
    final int imageWidth = (int)(imageSize.width * resolutionScale);
    final int imageHeight = (int)(imageSize.height * resolutionScale);
    this.qualitySlider = new JSlider(1, controller.getQualityLevelCount()) {
        @Override
        public String getToolTipText(MouseEvent ev) {
          float valueUnderMouse = getSliderValueAt(this, ev.getX(), preferences);
          float valueToTick = valueUnderMouse - (float)Math.floor(valueUnderMouse);
          if (valueToTick < 0.25f || valueToTick > 0.75f) {
            // Display a tooltip that explains the different quality levels
            return "<html><table><tr valign='middle'>"
                + "<td><img border='1' width='" + imageWidth + "' height='" + imageHeight + "' src='"
                + new ResourceURLContent(PhotoSizeAndQualityPanel.class, "resources/quality" + Math.round(valueUnderMouse - qualitySlider.getMinimum()) + ".jpg").getURL() + "'></td>"
                + "<td>" + preferences.getLocalizedString(PhotoSizeAndQualityPanel.class, "quality" + Math.round(valueUnderMouse - qualitySlider.getMinimum()) + "DescriptionLabel.text") + "</td>"
                + "</tr></table>";
          } else {
            return null;
          }
        }
      };
    // Add a listener that displays also the tool tip when user clicks on the slider
    this.qualitySlider.addMouseListener(new MouseAdapter() {
        @Override
        public void mousePressed(final MouseEvent ev) {
          EventQueue.invokeLater(new Runnable() {
              public void run() {
                float valueUnderMouse = getSliderValueAt(qualitySlider, ev.getX(), preferences);
                if (qualitySlider.getValue() == Math.round(valueUnderMouse)) {
                  ToolTipManager toolTipManager = ToolTipManager.sharedInstance();
                  int initialDelay = toolTipManager.getInitialDelay();
                  toolTipManager.setInitialDelay(Math.min(initialDelay, 150));
                  toolTipManager.mouseMoved(ev);
                  toolTipManager.setInitialDelay(initialDelay);
                }
              }
            });
        }
      });
    this.qualitySlider.setPaintTicks(true);
    this.qualitySlider.setMajorTickSpacing(1);
    this.qualitySlider.setSnapToTicks(true);
    final boolean offScreenImageSupported = Component3DManager.getInstance().isOffScreenImageSupported();
    this.qualitySlider.addChangeListener(new ChangeListener() {
        public void stateChanged(ChangeEvent ev) {
          if (!offScreenImageSupported) {
            // Can't support 2 first quality levels if offscreen image isn't supported
            qualitySlider.setValue(Math.max(qualitySlider.getMinimum() + 2, qualitySlider.getValue()));
          }
          controller.setQuality(qualitySlider.getValue() - qualitySlider.getMinimum());
        }
      });
    controller.addPropertyChangeListener(AbstractPhotoController.Property.QUALITY,
        new PropertyChangeListener() {
          public void propertyChange(PropertyChangeEvent ev) {
            qualitySlider.setValue(qualitySlider.getMinimum() + controller.getQuality());
          }
        });
    this.qualitySlider.setValue(this.qualitySlider.getMinimum() + controller.getQuality());

    // Listener on 3D view notified when its size changes
    final JComponent view3D = (JComponent)controller.get3DView();
    final ComponentAdapter view3DSizeListener = new ComponentAdapter() {
        @Override
        public void componentResized(ComponentEvent ev) {
          controller.set3DViewAspectRatio((float)view3D.getWidth() / view3D.getHeight());
        }
      };
    addAncestorListener(new AncestorListener() {
        public void ancestorAdded(AncestorEvent ev) {
          view3D.addComponentListener(view3DSizeListener);
          ToolTipManager.sharedInstance().registerComponent(qualitySlider);
        }

        public void ancestorRemoved(AncestorEvent ev) {
          ToolTipManager.sharedInstance().unregisterComponent(qualitySlider);
          view3D.removeComponentListener(view3DSizeListener);
        }

        public void ancestorMoved(AncestorEvent ev) {
        }
      });

    setComponentTexts(preferences);
  }

  /**
   * Returns the slider value matching a given x.
   */
  private float getSliderValueAt(JSlider qualitySlider, int x, UserPreferences preferences) {
    int fastLabelOffset = 0;
    int bestLabelOffset = 0;
    int sliderWidth = qualitySlider.getWidth() - fastLabelOffset - bestLabelOffset;
    return qualitySlider.getMinimum()
        + (float)(x - (qualitySlider.getComponentOrientation().isLeftToRight()
                          ? fastLabelOffset
                          : bestLabelOffset))
        / sliderWidth * (qualitySlider.getMaximum() - qualitySlider.getMinimum());
  }

  /**
   * Sets the texts of the components.
   */
  private void setComponentTexts(UserPreferences preferences) {
    this.widthLabel.setText(SwingTools.getLocalizedLabelText(preferences,
        PhotoSizeAndQualityPanel.class, "widthLabel.text"));
    this.heightLabel.setText(SwingTools.getLocalizedLabelText(preferences,
        PhotoSizeAndQualityPanel.class, "heightLabel.text"));
    this.applyProportionsCheckBox.setText(SwingTools.getLocalizedLabelText(preferences,
        PhotoSizeAndQualityPanel.class, "applyProportionsCheckBox.text"));
    this.qualityLabel.setText(SwingTools.getLocalizedLabelText(preferences,
        PhotoSizeAndQualityPanel.class, "qualityLabel.text"));
    this.fastQualityLabel.setText(SwingTools.getLocalizedLabelText(preferences,
        PhotoSizeAndQualityPanel.class, "fastLabel.text"));
    if (!Component3DManager.getInstance().isOffScreenImageSupported()) {
      this.fastQualityLabel.setEnabled(false);
    }
    this.bestQualityLabel.setText(SwingTools.getLocalizedLabelText(preferences,
        PhotoSizeAndQualityPanel.class, "bestLabel.text"));
  }

  /**
   * Sets components mnemonics and label / component associations.
   */
  private void setMnemonics(UserPreferences preferences) {
    if (!OperatingSystem.isMacOSX()) {
      this.widthLabel.setDisplayedMnemonic(KeyStroke.getKeyStroke(preferences.getLocalizedString(
          PhotoSizeAndQualityPanel.class, "widthLabel.mnemonic")).getKeyCode());
      this.widthLabel.setLabelFor(this.widthSpinner);
      this.heightLabel.setDisplayedMnemonic(KeyStroke.getKeyStroke(preferences.getLocalizedString(
          PhotoSizeAndQualityPanel.class, "heightLabel.mnemonic")).getKeyCode());
      this.heightLabel.setLabelFor(this.heightSpinner);
      this.applyProportionsCheckBox.setMnemonic(KeyStroke.getKeyStroke(preferences.getLocalizedString(
          PhotoSizeAndQualityPanel.class, "applyProportionsCheckBox.mnemonic")).getKeyCode());
      this.qualityLabel.setDisplayedMnemonic(KeyStroke.getKeyStroke(preferences.getLocalizedString(
          PhotoSizeAndQualityPanel.class, "qualityLabel.mnemonic")).getKeyCode());
      this.qualityLabel.setLabelFor(this.qualitySlider);
    }
  }

  /**
   * Preferences property listener bound to this panel with a weak reference to avoid
   * strong link between user preferences and this panel.
   */
  public static class LanguageChangeListener implements PropertyChangeListener {
    private final WeakReference<PhotoSizeAndQualityPanel> photoPanel;

    public LanguageChangeListener(PhotoSizeAndQualityPanel photoPanel) {
      this.photoPanel = new WeakReference<PhotoSizeAndQualityPanel>(photoPanel);
    }

    public void propertyChange(PropertyChangeEvent ev) {
      // If photo panel was garbage collected, remove this listener from preferences
      PhotoSizeAndQualityPanel photoPanel = this.photoPanel.get();
      UserPreferences preferences = (UserPreferences)ev.getSource();
      if (photoPanel == null) {
        preferences.removePropertyChangeListener(UserPreferences.Property.LANGUAGE, this);
      } else {
        photoPanel.setComponentOrientation(ComponentOrientation.getOrientation(Locale.getDefault()));
        photoPanel.setComponentTexts(preferences);
        photoPanel.setMnemonics(preferences);
      }
    }
  }

  /**
   * Layouts panel components in panel with their labels.
   */
  private void layoutComponents() {
    int labelAlignment = OperatingSystem.isMacOSX()
        ? JLabel.TRAILING
        : JLabel.LEADING;
    int standardGap = Math.round(5 * SwingTools.getResolutionScale());
    // First row
    Insets labelInsets = new Insets(0, 0, 0, standardGap);
    add(this.widthLabel, new GridBagConstraints(
        0, 0, 1, 1, 0, 0, GridBagConstraints.CENTER,
        GridBagConstraints.HORIZONTAL, labelInsets, 0, 0));
    // Use HORIZONTAL fill constraint with label alignment to ensure
    // label is correctly sized in small dialogs
    this.widthLabel.setHorizontalAlignment(labelAlignment);
    Insets componentInsets = new Insets(0, 0, 0, 10);
    add(this.widthSpinner, new GridBagConstraints(
        1, 0, 1, 1, 0, 0, GridBagConstraints.LINE_START,
        GridBagConstraints.HORIZONTAL, componentInsets, 0, 0));
    add(this.heightLabel, new GridBagConstraints(
        2, 0, 1, 1, 0, 0, GridBagConstraints.CENTER,
        GridBagConstraints.HORIZONTAL, labelInsets, 0, 0));
    this.heightLabel.setHorizontalAlignment(labelAlignment);
    add(this.heightSpinner, new GridBagConstraints(
        3, 0, 1, 1, 0, 0, GridBagConstraints.LINE_START,
        GridBagConstraints.HORIZONTAL, new Insets(0, 0, 0, 0), 0, 0));
    // Second row
    JPanel proportionsPanel = new JPanel();
    proportionsPanel.add(this.applyProportionsCheckBox);
    proportionsPanel.add(this.aspectRatioComboBox);
    add(proportionsPanel, new GridBagConstraints(
        0, 1, 4, 1, 0, 0, GridBagConstraints.CENTER,
        GridBagConstraints.HORIZONTAL, new Insets(0, 0, 0, 0), 0, 0));
    // Third row
    add(this.qualityLabel, new GridBagConstraints(
        0, 3, 1, 1, 0, 0, GridBagConstraints.CENTER,
        GridBagConstraints.HORIZONTAL, new Insets(0, 0, 0, standardGap), 0, 0));
    this.qualityLabel.setHorizontalAlignment(labelAlignment);
    add(this.qualitySlider, new GridBagConstraints(
        1, 3, 3, 1, 0, 0, GridBagConstraints.LINE_START,
        GridBagConstraints.HORIZONTAL, new Insets(0, 0, 0, 0), 0, 0));
    // Fourth row
    JPanel qualityLabelsPanel = new JPanel(new BorderLayout(20, 0));
    qualityLabelsPanel.add(this.fastQualityLabel, BorderLayout.WEST);
    qualityLabelsPanel.add(this.bestQualityLabel, BorderLayout.EAST);
    add(qualityLabelsPanel, new GridBagConstraints(
        1, 4, 3, 1, 0, 0, GridBagConstraints.CENTER,
        GridBagConstraints.HORIZONTAL, new Insets(OperatingSystem.isWindows() ? 0 : -3, 0, 0, 0), 0, 0));
  }

  /**
   * Enables or disables this panel and its components.
   */
  @Override
  public void setEnabled(boolean enabled) {
    super.setEnabled(enabled);
    this.widthSpinner.setEnabled(enabled);
    this.heightSpinner.setEnabled(enabled);
    this.applyProportionsCheckBox.setEnabled(enabled);
    this.aspectRatioComboBox.setEnabled(enabled);
    this.qualitySlider.setEnabled(enabled);
  }

  /**
   * Enables or disables components that allow to force proportions.
   */
  public void setProportionsChoiceEnabled(boolean enabled) {
    this.applyProportionsCheckBox.setEnabled(enabled);
    this.aspectRatioComboBox.setEnabled(enabled && this.applyProportionsCheckBox.isSelected());
  }
}
