/*
 * Created on 02.04.2005
 */
package net.sourceforge.ganttproject.gui.options;

import java.awt.BorderLayout;
import java.awt.CardLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.ComponentOrientation;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.FocusAdapter;
import java.awt.event.FocusEvent;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.File;
import java.math.BigDecimal;
import java.text.DecimalFormat;
import java.text.NumberFormat;
import java.text.ParseException;
import java.util.Arrays;
import java.util.Date;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;

import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.BorderFactory;
import javax.swing.Box;
import javax.swing.ButtonGroup;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JColorChooser;
import javax.swing.JComboBox;
import javax.swing.JComponent;
import javax.swing.JDialog;
import javax.swing.JFormattedTextField;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JPasswordField;
import javax.swing.JProgressBar;
import javax.swing.JRadioButton;
import javax.swing.JSlider;
import javax.swing.JTextField;
import javax.swing.SpringLayout;
import javax.swing.SwingConstants;
import javax.swing.SwingUtilities;
import javax.swing.UIManager;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;

import net.sourceforge.ganttproject.action.CancelAction;
import net.sourceforge.ganttproject.action.OkAction;
import net.sourceforge.ganttproject.gui.GPColorChooser;
import net.sourceforge.ganttproject.gui.TextFieldAndFileChooserComponent;
import net.sourceforge.ganttproject.gui.UIFacade;
import net.sourceforge.ganttproject.gui.UIFacade.Dialog;
import net.sourceforge.ganttproject.gui.UIUtil;
import net.sourceforge.ganttproject.language.GanttLanguage;

import org.jdesktop.swingx.JXDatePicker;
import org.jdesktop.swingx.JXHyperlink;

import biz.ganttproject.core.option.BooleanOption;
import biz.ganttproject.core.option.ChangeValueEvent;
import biz.ganttproject.core.option.ChangeValueListener;
import biz.ganttproject.core.option.ColorOption;
import biz.ganttproject.core.option.DateOption;
import biz.ganttproject.core.option.DefaultBooleanOption;
import biz.ganttproject.core.option.DefaultEnumerationOption;
import biz.ganttproject.core.option.DefaultIntegerOption;
import biz.ganttproject.core.option.DoubleOption;
import biz.ganttproject.core.option.EnumerationOption;
import biz.ganttproject.core.option.FileOption;
import biz.ganttproject.core.option.FontOption;
import biz.ganttproject.core.option.FontSpec;
import biz.ganttproject.core.option.GPOption;
import biz.ganttproject.core.option.GPOptionGroup;
import biz.ganttproject.core.option.IntegerOption;
import biz.ganttproject.core.option.MoneyOption;
import biz.ganttproject.core.option.StringOption;
import biz.ganttproject.core.option.ValidationException;

import com.google.common.base.Function;
import com.google.common.base.Objects;
import com.google.common.base.Strings;
import com.google.common.base.Supplier;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Maps;

/**
 * @author bard
 */
public class OptionsPageBuilder {
  I18N myi18n = new I18N();
  private Component myParentComponent;
  private final LayoutApi myLayoutApi;
  private UIFacade myUiFacade;
  private DecimalFormat myFormat;

  public static interface LayoutApi {
    void layout(JPanel panel, int componentsCount);
  }

  public static LayoutApi TWO_COLUMN_LAYOUT = new LayoutApi() {
    @Override
    public void layout(JPanel panel, int componentsCount) {
      panel.setLayout(new SpringLayout());
      SpringUtilities.makeCompactGrid(panel, componentsCount, 2, 0, 0, 5, 3);
    }
  };

  public static LayoutApi ONE_COLUMN_LAYOUT = new LayoutApi() {
    @Override
    public void layout(JPanel panel, int componentsCount) {
      panel.setLayout(new SpringLayout());
      SpringUtilities.makeCompactGrid(panel, componentsCount*2, 1, 0, 0, 5, new Function<Integer, Integer>() {
        @Override
        public Integer apply(Integer input) {
          return input % 2 == 0 ? 5 : 3;
        }
      });
    }
  };

  public OptionsPageBuilder() {
    this(null, TWO_COLUMN_LAYOUT);
  }

  public OptionsPageBuilder(Component parentComponent, LayoutApi layoutApi) {
    myParentComponent = parentComponent;
    myLayoutApi = layoutApi;
  }

  public void setUiFacade(UIFacade uiFacade) {
    myUiFacade = uiFacade;
  }

  public void setI18N(I18N i18n) {
    myi18n = i18n;
  }

  public I18N getI18N() {
    return myi18n;
  }

  public void setOptionKeyPrefix(String optionKeyPrefix) {
    myi18n.myOptionKeyPrefix = optionKeyPrefix;
  }


  public JComponent buildPage(GPOptionGroup[] optionGroups, String pageID) {
    JComponent topPanel = TopPanel.create(myi18n.getPageTitle(pageID), myi18n.getPageDescription(pageID));
    JComponent planePage = buildPlanePage(optionGroups);
    return UIUtil.createTopAndCenter(topPanel, planePage);
  }

  public JComponent buildPlanePage(GPOptionGroup[] optionGroups) {
    final JComponent optionsPanel = new JPanel(new SpringLayout());
    for (int i = 0; i < optionGroups.length; i++) {
      optionsPanel.add(createGroupComponent(optionGroups[i]));
    }
    SpringUtilities.makeCompactGrid(optionsPanel, optionGroups.length, 1, 0, 0, 5, 15);
    JPanel resultPanel = new JPanel(new BorderLayout());
    resultPanel.add(optionsPanel, BorderLayout.NORTH);
    resultPanel.addFocusListener(new FocusAdapter() {
      @Override
      public void focusGained(FocusEvent e) {
        optionsPanel.getComponent(0).requestFocus();
      }

    });
    return resultPanel;
  }

  public JComponent createLabeledComponent(GPOption<?> option) {
    GPOptionGroup fake = new GPOptionGroup("", new GPOption[] { option });
    fake.setTitled(false);
    return createGroupComponent(fake);
  }

  public JComponent createGroupComponent(GPOptionGroup group) {
    GPOption<?>[] options = group.getOptions();
    JComponent optionsPanel = createGroupComponent(group, options);
    if (group.isTitled()) {
      UIUtil.createTitle(optionsPanel, myi18n.getOptionGroupLabel(group));
    }
    JPanel result = new JPanel(new BorderLayout());
    result.add(optionsPanel, BorderLayout.NORTH);
    return result;
  }

  public JComponent createGroupComponent(GPOptionGroup group, GPOption<?>... options) {
    JPanel optionsPanel = new JPanel();

    int hasUiCount = 0;
    for (int i = 0; i < options.length; i++) {
      GPOption<?> nextOption = options[i];
      if (!nextOption.hasUi()) {
        continue;
      }
      hasUiCount++;
      final Component nextComponent = createOptionComponent(group, nextOption);
      if (needsLabel(group, nextOption)) {
        Component nextLabel = createOptionLabel(group, options[i]);
        optionsPanel.add(nextLabel);
        optionsPanel.add(nextComponent);
      } else {
        optionsPanel.add(nextComponent);
        optionsPanel.add(new JPanel());
      }
      if (i == 0) {
        optionsPanel.addFocusListener(new FocusAdapter() {
          @Override
          public void focusGained(FocusEvent e) {
            super.focusGained(e);
            nextComponent.requestFocus();
          }

        });
      }
    }
    if (hasUiCount > 0) {
      myLayoutApi.layout(optionsPanel, hasUiCount);
    }
    return optionsPanel;
  }

  private boolean needsLabel(GPOptionGroup group, GPOption<?> nextOption) {
    // if (nextOption instanceof BooleanOption) {
    // return !isCheckboxOption(group, nextOption);
    // }
    return true;
  }

  public Component createStandaloneOptionPanel(GPOption<?> option) {
    JPanel optionPanel = new JPanel(new BorderLayout());
    Component optionComponent = createOptionComponent(null, option);
    if (needsLabel(null, option)) {
      optionPanel.add(createOptionLabel(null, option), BorderLayout.WEST);
      optionPanel.add(optionComponent, BorderLayout.CENTER);
    } else {
      optionPanel.add(optionComponent, BorderLayout.WEST);
    }
    JPanel result = new JPanel(new BorderLayout());
    result.add(optionPanel, BorderLayout.NORTH);
    return result;
  }

  public Component createWaitIndicatorComponent(DefaultBooleanOption controller) {
    final JProgressBar progressBar = new JProgressBar();
    JPanel placeholder = new JPanel();
    final JPanel result = new JPanel(new CardLayout());
    result.add(placeholder, "placeholder");
    result.add(progressBar, "progressBar");
    controller.addChangeValueListener(new ChangeValueListener() {
      @Override
      public void changeValue(ChangeValueEvent event) {
        if (Boolean.TRUE.equals(event.getNewValue())) {
          progressBar.setIndeterminate(true);
          ((CardLayout) result.getLayout()).show(result, "progressBar");
        } else {
          progressBar.setIndeterminate(false);
          ((CardLayout) result.getLayout()).show(result, "placeholder");
        }
      }
    });
    return result;
  }

  public Component createOptionLabel(GPOptionGroup group, GPOption<?> option) {
    JLabel nextLabel = new JLabel(myi18n.getOptionLabel(group, option));
    nextLabel.setVerticalAlignment(SwingConstants.TOP);
    nextLabel.setBorder(BorderFactory.createEmptyBorder(2, 0, 0, 0));
    return nextLabel;
  }

  public Component createOptionComponent(GPOptionGroup group, GPOption<?> option) {
    Component result = null;
    if (option instanceof EnumerationOption) {
      result = createEnumerationComponent((EnumerationOption) option, group);
    } else if (option instanceof FileOption) {
      result = createFileComponent((FileOption) option);
    } else if (option instanceof BooleanOption) {
      result = createBooleanComponent(group, (BooleanOption) option);
    } else if (option instanceof ColorOption) {
      result = createColorComponent((ColorOption) option).getJComponent();
    } else if (option instanceof DateOption) {
      result = createDateComponent((DateOption) option);
    } else if (option instanceof GPOptionGroup) {
      result = createButtonComponent((GPOptionGroup) option);
    } else if (option instanceof StringOption) {
      result = createStringComponent((StringOption) option);
    } else if (option instanceof IntegerOption) {
      result = createValidatingComponent((IntegerOption) option, new ValueValidator<Integer>() {
        @Override
        public Integer parse(String text) {
          return Integer.valueOf(text);
        }
      });
    } else if (option instanceof DoubleOption) {
      result = createValidatingComponent((DoubleOption) option, new ValueValidator<Double>() {
        @Override
        public Double parse(String text) {
          return Double.valueOf(text);
        }
      });
    } else if (option instanceof MoneyOption) {
      result = createValidatingComponent((MoneyOption) option, new ValueValidator<BigDecimal>() {
        private NumberFormat myFormat;
        {
          DecimalFormat format = (DecimalFormat) NumberFormat.getNumberInstance(GanttLanguage.getInstance().getLocale());
          format.setParseBigDecimal(true);
          myFormat = format;
        }
        @Override
        public BigDecimal parse(String text) throws ValidationException {
          try {
            return Strings.isNullOrEmpty(text) ? BigDecimal.ZERO : (BigDecimal) myFormat.parse(text);
          } catch (ParseException e) {
            e.printStackTrace();
            throw new ValidationException(e);
          }
        }
      });
    } else if (option instanceof FontOption) {
      result = createFontComponent((FontOption)option);
    }
    if (result == null) {
      result = new JLabel("Unknown option class=" + option.getClass());
    }
    result.setEnabled(option.isWritable());
    final Component finalResult = result;
    option.addPropertyChangeListener(new PropertyChangeListener() {
      @Override
      public void propertyChange(PropertyChangeEvent evt) {
        if ("isWritable".equals(evt.getPropertyName())) {
          assert evt.getNewValue() instanceof Boolean : "Unexpected value of property isWritable: " + evt.getNewValue();
          finalResult.setEnabled((Boolean) evt.getNewValue());
        }
      }
    });
    return result;
  }

  private static Color getValidFieldColor() {
    return UIUtil.getValidFieldColor();
  }

  private static void updateTextField(final JTextField textField, final DocumentListener listener,
      final ChangeValueEvent event) {
    SwingUtilities.invokeLater(new Runnable() {
      @Override
      public void run() {
        textField.getDocument().removeDocumentListener(listener);
        if (!textField.getText().equals(event.getNewValue())) {
          String newText = String.valueOf(event.getNewValue());
          textField.setText(newText);
        }
        textField.getDocument().addDocumentListener(listener);
      }
    });
  }

  private Component createFileComponent(final FileOption option) {
    final TextFieldAndFileChooserComponent result = new TextFieldAndFileChooserComponent(myUiFacade, myi18n.getValue(myi18n.myOptionKeyPrefix + option.getID() + ".dialogTitle")) {
      @Override
      protected void onFileChosen(File file) {
        option.setValue(file.getAbsolutePath());
      }
    };
    if (option.getValue() != null) {
      result.setFile(new File(option.getValue()));
    }
    option.addChangeValueListener(new ChangeValueListener() {
      @Override
      public void changeValue(ChangeValueEvent event) {
        result.setFile(new File(String.valueOf(event.getNewValue())));
      }
    });
    return result;
  }

  private Component createStringComponent(final StringOption option) {
    final JTextField result = option.isScreened() ? new JPasswordField(option.getValue()) : new JTextField(option.getValue());

    final DocumentListener documentListener = new DocumentListener() {
      private void saveValue() {
        try {
          option.setValue(result.getText());
          result.setBackground(getValidFieldColor());
        } catch (ValidationException ex) {
          result.setBackground(UIUtil.INVALID_FIELD_COLOR);
        }
      }

      @Override
      public void insertUpdate(DocumentEvent e) {
        saveValue();
      }

      @Override
      public void removeUpdate(DocumentEvent e) {
        saveValue();
      }

      @Override
      public void changedUpdate(DocumentEvent e) {
        saveValue();
      }
    };
    result.getDocument().addDocumentListener(documentListener);
    option.addChangeValueListener(new ChangeValueListener() {
      @Override
      public void changeValue(final ChangeValueEvent event) {
        updateTextField(result, documentListener, event);
      }
    });
    return result;
  }

  private Component createButtonComponent(GPOptionGroup optionGroup) {
    Action action = new AbstractAction(myi18n.getAdvancedActionTitle()) {
      @Override
      public void actionPerformed(ActionEvent e) {
        System.err.println("[OptionsPageBuilder] createButtonComponent: ");
      }

    };
    JButton result = new JButton(action);
    return result;
  }

  private Component createBooleanComponent(GPOptionGroup group, final BooleanOption option) {
    if (!isCheckboxOption(group, option)) {
      return createRadioButtonBooleanComponent(group, option);
    }
    final JCheckBox result = new JCheckBox(new BooleanOptionAction(option));
    String trailingLabel = getTrailingLabel(option);
    if (trailingLabel != null) {
      result.setText(trailingLabel);
    }
    result.setHorizontalAlignment(JCheckBox.LEFT);
    result.setHorizontalTextPosition(SwingConstants.TRAILING);
    result.setSelected(option.isChecked());
    ComponentOrientation componentOrientation = GanttLanguage.getInstance().getComponentOrientation();
    result.setComponentOrientation(componentOrientation);
    option.addChangeValueListener(new ChangeValueListener() {
      @Override
      public void changeValue(ChangeValueEvent event) {
        result.setSelected(option.getValue());
      }
    });
    return result;
  }

  private String getTrailingLabel(BooleanOption option) {
    String trailingLabelKey = myi18n.getCanonicalOptionLabelKey(option) + ".trailing";
    return myi18n.hasValue(trailingLabelKey) ? myi18n.getValue(trailingLabelKey) : null;
  }

  private boolean isCheckboxOption(GPOptionGroup group, GPOption<?> option) {
    String yesKey = myi18n.getCanonicalOptionLabelKey(option) + ".yes";
    if ((group == null || group.getI18Nkey(yesKey) == null) && !myi18n.hasValue(yesKey)) {
      return true;
    }
    String noKey = myi18n.getCanonicalOptionLabelKey(option) + ".no";
    if ((group == null || group.getI18Nkey(noKey) == null) && !myi18n.hasValue(noKey)) {
      return true;
    }
    return false;
  }

  public static class BooleanOptionRadioUi {
    private final JRadioButton myYesButton;
    private final JRadioButton myNoButton;

    private BooleanOptionRadioUi(final BooleanOption option) {
      myYesButton = new JRadioButton(new AbstractAction("") {
        @Override
        public void actionPerformed(ActionEvent e) {
          if (!option.isChecked()) {
            option.setValue(true);
          }
        }
      });
      myYesButton.setVerticalAlignment(SwingConstants.CENTER);
      myYesButton.setSelected(option.isChecked());

      myNoButton = new JRadioButton(new AbstractAction("") {
        @Override
        public void actionPerformed(ActionEvent e) {
          if (option.isChecked()) {
            option.setValue(false);
          }
        }
      });
      myNoButton.setSelected(!option.isChecked());

      ButtonGroup buttonGroup = new ButtonGroup();
      buttonGroup.add(myYesButton);
      buttonGroup.add(myNoButton);

      option.addChangeValueListener(new ChangeValueListener() {
        @Override
        public void changeValue(ChangeValueEvent event) {
          if (Boolean.TRUE.equals(event.getNewValue())) {
            myYesButton.setSelected(true);
          } else {
            myNoButton.setSelected(true);
          }
        }
      });
    }

    public JRadioButton getYesButton() {
      return myYesButton;
    }

    public JRadioButton getNoButton() {
      return myNoButton;
    }

    public Component getComponent() {
      Box result = Box.createVerticalBox();
      result.add(myYesButton);
      result.add(Box.createVerticalStrut(2));
      result.add(myNoButton);
      result.add(Box.createVerticalGlue());
      return result;
    }
  }

  public static BooleanOptionRadioUi createBooleanOptionRadioUi(BooleanOption option) {
    return new BooleanOptionRadioUi(option);
  }

  private Component createRadioButtonBooleanComponent(GPOptionGroup group, final BooleanOption option) {
    BooleanOptionRadioUi radioUi = createBooleanOptionRadioUi(option);
    radioUi.getYesButton().setText(myi18n.getValue(group, myi18n.getCanonicalOptionLabelKey(option) + ".yes"));
    radioUi.getNoButton().setText(myi18n.getValue(group, myi18n.getCanonicalOptionLabelKey(option) + ".no"));
    return radioUi.getComponent();
  }

  private JComboBox createEnumerationComponent(final EnumerationOption option, final GPOptionGroup group) {
    return createEnumerationComponent(option, new Supplier<EnumerationOptionComboBoxModel>() {
      @Override
      public EnumerationOptionComboBoxModel get() {
        return new EnumerationOptionComboBoxModel(option, group);
      }
    });
  }

  private JComboBox createEnumerationComponent(final EnumerationOption option, final Supplier<EnumerationOptionComboBoxModel> modelFactory) {
    final JComboBox result = new JComboBox(modelFactory.get());
    option.addChangeValueListener(new ChangeValueListener() {
      @Override
      public void changeValue(ChangeValueEvent event) {
        EnumerationOptionComboBoxModel model = (EnumerationOptionComboBoxModel) result.getModel();
        model.onValueChange();
        result.setSelectedItem(model.getSelectedItem());
      }
    });
    option.addPropertyChangeListener(new PropertyChangeListener() {
      @Override
      public void propertyChange(PropertyChangeEvent evt) {
        if (EnumerationOption.VALUE_SET.equals(evt.getPropertyName())) {
          EnumerationOptionComboBoxModel model = modelFactory.get();
          result.setModel(model);
          result.setSelectedItem(model.getSelectedItem());
        }
      }
    });
    return result;
  }

  public static interface ColorComponent {
    JComponent getJComponent();
    void openChooser();
    void setOnCancelCallback(Runnable onCancel);
    void setOnOkCallback(Runnable runnable);
  }
  public ColorComponent createColorComponent(final ColorOption option) {
    final JXHyperlink colorButton = new JXHyperlink();
    final JPanel label = new JPanel();
    label.setPreferredSize(new Dimension(16, 16));
    label.setBackground(option.getValue());

    option.addChangeValueListener(new ChangeValueListener() {
      @Override
      public void changeValue(ChangeValueEvent event) {
        label.setBackground(option.getValue());
      }
    });
    final AtomicReference<Runnable> onOk = new AtomicReference<>();
    final AtomicReference<Runnable> onCancel = new AtomicReference<>();
    final Action action = new AbstractAction(myi18n.getColorButtonText(option)) {
      @Override
      public void actionPerformed(ActionEvent e) {
        final GPColorChooser colorChooser = new GPColorChooser();
        OkAction okAction = new OkAction() {
          @Override
          public void actionPerformed(ActionEvent arg0) {
            Color color = colorChooser.getColor();
            label.setBackground(color);
            option.setValue(color);
            List<Color> recentColors = GPColorChooser.getRecentColors();
            // If we already have such color in the recent list then we remove it
            // If we failed to remove selected color (=> it was not in the list)
            // and the list is long enough then we remove colors from the tail
            if (!recentColors.remove(color) && recentColors.size() == 10) {
              recentColors.remove(9);
            }
            recentColors.add(0,  color);
            GPColorChooser.setRecentColors(recentColors);
            if (onOk.get() != null) {
              onOk.get().run();
            }
          }
        };
        final CancelAction cancelAction = new CancelAction() {
          @Override
          public void actionPerformed(ActionEvent e) {
            super.actionPerformed(e);
            if (onCancel.get() != null) {
              onCancel.get().run();
            }
          }
        };
        colorChooser.setColor(colorButton.getBackground());
        Dialog dialog = myUiFacade.createDialog(
            colorChooser.buildComponent(),
            new Action[] {okAction, cancelAction},
            myi18n.getColorChooserTitle(option));
        dialog.show();
      };
    };
    colorButton.setAction(action);

    final JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.LEADING, 0, 0));
    buttonPanel.add(label);
    buttonPanel.add(new JLabel(" "));
    buttonPanel.add(colorButton);
    return new ColorComponent() {
      @Override
      public void openChooser() {
        action.actionPerformed(null);
      }
      @Override
      public JComponent getJComponent() {
        return buttonPanel;
      }
      @Override
      public void setOnCancelCallback(Runnable runnable) {
        onCancel.set(runnable);
      }
      @Override
      public void setOnOkCallback(Runnable runnable) {
        onOk.set(runnable);
      }
    };
  }

  public Component createFontComponent(final FontOption option) {
    final Object CLIENT_ID = new Object();
    final DefaultEnumerationOption<String> familiesOption = new DefaultEnumerationOption<>("", option.getFontFamilies());
    if (option.getValue() != null) {
      familiesOption.setValue(option.getValue().getFamily());
    }

    JComboBox comboBox = createEnumerationComponent(familiesOption, new Supplier<EnumerationOptionComboBoxModel>() {
      @Override
      public EnumerationOptionComboBoxModel get() {
        return new EnumerationOptionComboBoxModel(familiesOption, option.getFontFamilies().toArray(new String[0]));
      }
    });
    final JSlider slider = new JSlider(0, 4);
    slider.setPaintTicks(false);
    Hashtable<Integer, JComponent> labels = new Hashtable<Integer, JComponent>();
    for (Map.Entry<FontSpec.Size, String> label : option.getSizeLabels().entrySet()) {
      labels.put(label.getKey().ordinal(), new JLabel(label.getValue()));
    }
    slider.setLabelTable(labels);
    slider.setPaintLabels(true);
    if (option.getValue() != null) {
      slider.setValue(option.getValue().getSize().ordinal());
    }

    ChangeValueListener uiChangeListener = new ChangeValueListener() {
      @Override
      public void changeValue(ChangeValueEvent event) {
        option.setValue(new FontSpec(familiesOption.getValue(), FontSpec.Size.values()[slider.getValue()]), CLIENT_ID);
      }
    };
    familiesOption.addChangeValueListener(uiChangeListener);
    slider.addChangeListener(new ChangeListener() {
      public void stateChanged(ChangeEvent arg0) {
        option.setValue(new FontSpec(familiesOption.getValue(), FontSpec.Size.values()[slider.getValue()]), CLIENT_ID);
      }
    });

    final Box buttonPanel = Box.createVerticalBox();
    buttonPanel.add(comboBox);
    buttonPanel.add(slider);
    buttonPanel.setBorder(BorderFactory.createEmptyBorder(0, 0, 5, 0));
    return buttonPanel;
  }


  public JComponent createDateComponent(final DateOption option) {
    class OptionValueUpdater implements ActionListener, PropertyChangeListener {
      @Override
      public void actionPerformed(ActionEvent e) {
        option.setValue(((JXDatePicker) e.getSource()).getDate());
      }

      @Override
      public void propertyChange(PropertyChangeEvent evt) {
        if (evt.getNewValue() instanceof Date && !evt.getNewValue().equals(option.getValue())) {
          option.setValue((Date) evt.getNewValue());
        }
      }
    }
    OptionValueUpdater valueUpdater = new OptionValueUpdater();
    final JXDatePicker result = UIUtil.createDatePicker();
    UIUtil.setupDatePicker(result, option.getValue(), null, valueUpdater);
    result.setDate(option.getValue());
    result.getEditor().addPropertyChangeListener("value", valueUpdater);

    option.addChangeValueListener(new ChangeValueListener() {
      @Override
      public void changeValue(ChangeValueEvent event) {
        assert event.getNewValue() == null || event.getNewValue() instanceof Date : "value=" + event.getNewValue();
        result.setDate((Date) event.getNewValue());
      }
    });

    return result;
  }

  public interface ValueValidator<T> {
    T parse(String text) throws ValidationException;
  }

  /**
   * Create JTextField component in options that allows user to input only
   * integer values.
   *
   * @param option
   * @return
   */
  public static <T extends Number> Component createValidatingComponent(final GPOption<T> option, final ValueValidator<T> parser) {
    final JTextField result = new JTextField(String.valueOf(option.getValue()));
    final DocumentListener listener = UIUtil.attachValidator(result, parser, option);
    option.addChangeValueListener(new ChangeValueListener() {
      @Override
      public void changeValue(final ChangeValueEvent event) {
        if (!Objects.equal(parser, event.getTriggerID())) {
          updateTextField(result, listener, event);
        }
      }
    });
    return result;
  }

  public static class I18N {
    private String myOptionKeyPrefix = "option.";
    private String myOptionGroupKeyPrefix = "optionGroup.";
    private String myOptionPageKeyPrefix = "optionPage.";

    public I18N() {
    }

    protected boolean hasValue(String key) {
      return GanttLanguage.getInstance().getText(key) != null;
    }
    protected String getValue(String key) {
      String result = GanttLanguage.getInstance().getText(key);
      return result == null ? key : result;
    }

    public String getValue(GPOptionGroup group, String canonicalKey) {
      String key = group == null ? null : group.getI18Nkey(canonicalKey);
      return getValue(key == null ? canonicalKey : key);
    }

    public String getPageTitle(String pageID) {
      return getValue(getCanonicalOptionPageTitleKey(pageID));
    }

    public String getPageDescription(String pageID) {
      return GanttLanguage.getInstance().getText(myOptionPageKeyPrefix + pageID + ".description");
    }

    public String getOptionGroupLabel(GPOptionGroup group) {
      String canonicalKey = getCanonicalOptionGroupLabelKey(group);
      return getValue(group, canonicalKey);
    }

    public String getOptionLabel(GPOptionGroup group, GPOption<?> option) {
      String result = null;
      if (group != null) {
        String keyWithGroup = String.format("%s%s.%s.label", myOptionKeyPrefix, group.getID(), option.getID());
        result = getValue(group, keyWithGroup);
        if (!Objects.equal(result, keyWithGroup)) {
          return result;
        }
      }
      String canonicalKey = getCanonicalOptionLabelKey(option);
      return getValue(group, canonicalKey);
    }

    public final String getCanonicalOptionPageLabelKey(String pageID) {
      return myOptionPageKeyPrefix + pageID + ".label";
    }

    public final String getCanonicalOptionPageTitleKey(String pageID) {
      return myOptionPageKeyPrefix + pageID + ".title";
    }

    public String getCanonicalOptionPageDescriptionKey(String pageID) {
      return myOptionPageKeyPrefix + pageID + ".description";
    }

    public final String getCanonicalOptionGroupLabelKey(GPOptionGroup group) {
      return myOptionGroupKeyPrefix + group.getID() + ".label";
    }

    public final String getCanonicalOptionLabelKey(GPOption<?> option) {
      return myOptionKeyPrefix + option.getID() + ".label";
    }

    public static final String getCanonicalOptionValueLabelKey(String valueID) {
      return "optionValue." + valueID + ".label";
    }

    String getAdvancedActionTitle() {
      return GanttLanguage.getInstance().getText("optionAdvanced.label");
    }

    String getColorButtonText(ColorOption colorOption) {
      return GanttLanguage.getInstance().getText("colorButton");
    }

    String getColorChooserTitle(ColorOption colorOption) {
      return GanttLanguage.getInstance().getText("selectColor");
    }
  }

  //private static GPColorChooser ourColorChooser = new GPColorChooser(ImmutableList.of(Color.BLACK, Color.RED, Color.GREEN, Color.BLUE));
}
