/**
 * L2FProd.com Common Components 7.3 License.
 *
 * Copyright 2005-2007 L2FProd.com
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.l2fprod.common.propertysheet;

import com.l2fprod.common.swing.IconPool;
import com.l2fprod.common.swing.LookAndFeelTweaks;
import com.l2fprod.common.swing.plaf.blue.BlueishButtonUI;
import com.l2fprod.common.util.ResourceManager;

import javax.swing.*;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.beans.BeanInfo;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.beans.PropertyDescriptor;
import java.util.Comparator;
import java.util.Iterator;
import java.util.Map;

/**
 * An implementation of a PropertySheet which shows a table to
 * edit/view values, a description pane which updates when the
 * selection changes and buttons to toggle between a flat view and a
 * by-category view of the properties. A button in the toolbar allows
 * to sort the properties and categories by name.
 * <p/>
 * Default sorting is by name (case-insensitive). Custom sorting can
 * be implemented through
 * {@link com.l2fprod.common.propertysheet.PropertySheetTableModel#setCategorySortingComparator(Comparator)}
 * and
 * {@link com.l2fprod.common.propertysheet.PropertySheetTableModel#setPropertySortingComparator(Comparator)}
 */
public class PropertySheetPanel extends JPanel implements PropertySheet, PropertyChangeListener {

  private PropertySheetTable table;
  private PropertySheetTableModel model;
  private JScrollPane tableScroll;
  private ListSelectionListener selectionListener = new SelectionListener();

  private JPanel actionPanel;
  private JToggleButton sortButton;
  private JToggleButton asCategoryButton;
  private JToggleButton descriptionButton;

  private JSplitPane split;
  private int lastDescriptionHeight;

  private JEditorPane descriptionPanel;
  private JScrollPane descriptionScrollPane;

  public PropertySheetPanel() {
    this(new PropertySheetTable());
  }

  public PropertySheetPanel(PropertySheetTable table) {
    buildUI();
    setTable(table);
  }

  /**
   * Sets the table used by this panel.
   * <p/>
   * Note: listeners previously added with
   * {@link PropertySheetPanel#addPropertySheetChangeListener(PropertyChangeListener)}
   * must be re-added after this call if the table model is not the
   * same as the previous table.
   *
   * @param table
   */
  public void setTable(PropertySheetTable table) {
    if (table == null) {
      throw new IllegalArgumentException("table must not be null");
    }

    // remove the property change listener from any previous model
    if (model != null)
      model.removePropertyChangeListener(this);

    // get the model from the table
    model = (PropertySheetTableModel) table.getModel();
    model.addPropertyChangeListener(this);

    // remove the listener from the old table
    if (this.table != null)
      this.table.getSelectionModel().removeListSelectionListener(selectionListener);

    // prepare the new table
    table.getSelectionModel().addListSelectionListener(selectionListener);
    tableScroll.getViewport().setView(table);

    // use the new table as our table
    this.table = table;
  }

  /**
   * React to property changes by repainting.
   */
  public void propertyChange(PropertyChangeEvent evt) {
    repaint();
  }

  /**
   * @return the table used to edit/view Properties.
   */
  public PropertySheetTable getTable() {
    return table;
  }

  /**
   * Toggles the visibility of the description panel.
   *
   * @param visible
   */
  public void setDescriptionVisible(boolean visible) {
    if (visible) {
      add("Center", split);
      split.setTopComponent(tableScroll);
      split.setBottomComponent(descriptionScrollPane);
      // restore the divider location
      split.setDividerLocation(split.getHeight() - lastDescriptionHeight);
    } else {
      // save the size of the description pane to restore it later
      lastDescriptionHeight = split.getHeight() - split.getDividerLocation();
      remove(split);
      add("Center", tableScroll);
    }
    descriptionButton.setSelected(visible);
    PropertySheetPanel.this.revalidate();
  }

  /**
   * Toggles the visibility of the toolbar panel
   *
   * @param visible
   */
  public void setToolBarVisible(boolean visible) {
    actionPanel.setVisible(visible);
    PropertySheetPanel.this.revalidate();
  }

  /**
   * Set the current mode, either {@link PropertySheet#VIEW_AS_CATEGORIES}
   * or {@link PropertySheet#VIEW_AS_FLAT_LIST}.
   */
  public void setMode(int mode) {
    model.setMode(mode);
    asCategoryButton.setSelected(PropertySheet.VIEW_AS_CATEGORIES == mode);
  }

  public void setProperties(Property[] properties) {
    model.setProperties(properties);
  }

  public Property[] getProperties() {
    return model.getProperties();
  }

  public void addProperty(Property property) {
    model.addProperty(property);
  }

  public void addProperty(int index, Property property) {
    model.addProperty(index, property);
  }

  public void removeProperty(Property property) {
    model.removeProperty(property);
  }

  public int getPropertyCount() {
    return model.getPropertyCount();
  }

  public Iterator propertyIterator() {
    return model.propertyIterator();
  }

  public void setBeanInfo(BeanInfo beanInfo) {
    setProperties(beanInfo.getPropertyDescriptors());
  }

  public void setProperties(PropertyDescriptor[] descriptors) {
    Property[] properties = new Property[descriptors.length];
    for (int i = 0, c = descriptors.length; i < c; i++) {
      properties[i] = new PropertyDescriptorAdapter(descriptors[i]);
    }
    setProperties(properties);
  }

  /**
   * Initializes the PropertySheet from the given object. If any, it cancels
   * pending edit before proceeding with properties.
   *
   * @param data
   */
  public void readFromObject(Object data) {
    // cancel pending edits
    getTable().cancelEditing();

    Property[] properties = model.getProperties();
    for (int i = 0, c = properties.length; i < c; i++) {
      properties[i].readFromObject(data);
    }
    repaint();
  }

  /**
   * Writes the PropertySheet to the given object. If any, it commits pending
   * edit before proceeding with properties.
   *
   * @param data
   */
  public void writeToObject(Object data) {
    // ensure pending edits are committed
    getTable().commitEditing();

    Property[] properties = getProperties();
    for (int i = 0, c = properties.length; i < c; i++) {
      properties[i].writeToObject(data);
    }
  }

  public void addPropertySheetChangeListener(PropertyChangeListener listener) {
    model.addPropertyChangeListener(listener);
  }

  public void removePropertySheetChangeListener(PropertyChangeListener listener) {
    model.removePropertyChangeListener(listener);
  }

  public void setEditorFactory(PropertyEditorFactory factory) {
    table.setEditorFactory(factory);
  }

  public PropertyEditorFactory getEditorFactory() {
    return table.getEditorFactory();
  }

  /**
   * @param registry
   * @deprecated use {@link #setEditorFactory(PropertyEditorFactory)}
   */
  public void setEditorRegistry(PropertyEditorRegistry registry) {
    table.setEditorFactory(registry);
  }

  /**
   * @deprecated use {@link #getEditorFactory()}
   */
  public PropertyEditorRegistry getEditorRegistry() {
    return (PropertyEditorRegistry) table.getEditorFactory();
  }

  public void setRendererFactory(PropertyRendererFactory factory) {
    table.setRendererFactory(factory);
  }

  public PropertyRendererFactory getRendererFactory() {
    return table.getRendererFactory();
  }

  /**
   * @param registry
   * @deprecated use {@link #setRendererFactory(PropertyRendererFactory)}
   */
  public void setRendererRegistry(PropertyRendererRegistry registry) {
    table.setRendererRegistry(registry);
  }

  /**
   * @deprecated use {@link #getRendererFactory()}
   */
  public PropertyRendererRegistry getRendererRegistry() {
    return table.getRendererRegistry();
  }

  /**
   * Sets sorting of categories enabled or disabled.
   *
   * @param value true to enable sorting
   */
  public void setSortingCategories(boolean value) {
    model.setSortingCategories(value);
    sortButton.setSelected(isSorting());
  }

  /**
   * Is sorting of categories enabled.
   *
   * @return true if category sorting is enabled
   */
  public boolean isSortingCategories() {
    return model.isSortingCategories();
  }

  /**
   * Sets sorting of properties enabled or disabled.
   *
   * @param value true to enable sorting
   */
  public void setSortingProperties(boolean value) {
    model.setSortingProperties(value);
    sortButton.setSelected(isSorting());
  }

  /**
   * Is sorting of properties enabled.
   *
   * @return true if property sorting is enabled
   */
  public boolean isSortingProperties() {
    return model.isSortingProperties();
  }

  /**
   * Sets sorting properties and categories enabled or disabled.
   *
   * @param value true to enable sorting
   */
  public void setSorting(boolean value) {
    model.setSortingCategories(value);
    model.setSortingProperties(value);
    sortButton.setSelected(value);
  }

  /**
   * @return true if properties or categories are sorted.
   */
  public boolean isSorting() {
    return model.isSortingCategories() || model.isSortingProperties();
  }

  /**
   * Sets the Comparator to be used with categories. Categories are
   * treated as String-objects.
   *
   * @param comp java.util.Comparator used to compare categories
   */
  public void setCategorySortingComparator(Comparator comp) {
    model.setCategorySortingComparator(comp);
  }

  /**
   * Sets the Comparator to be used with Property-objects.
   *
   * @param comp java.util.Comparator used to compare Property-objects
   */
  public void setPropertySortingComparator(Comparator comp) {
    model.setPropertySortingComparator(comp);
  }

  /**
   * Set wether or not toggle states are restored when new properties are
   * applied.
   *
   * @param value true to enable
   */
  public void setRestoreToggleStates(boolean value) {
    model.setRestoreToggleStates(value);
  }

  /**
   * @return true is toggle state restore is enabled
   */
  public boolean isRestoreToggleStates() {
    return model.isRestoreToggleStates();
  }

  /**
   * @return the category view toggle states.
   */
  public Map getToggleStates() {
    return model.getToggleStates();
  }

  /**
   * Sets the toggle states for the category views. Note this <b>MUST</b> be
   * called <b>BEFORE</b> setting any properties.
   *
   * @param toggleStates the toggle states as returned by getToggleStates
   */
  public void setToggleStates(Map toggleStates) {
    model.setToggleStates(toggleStates);
  }

  private void buildUI() {
    LookAndFeelTweaks.setBorderLayout(this);
    LookAndFeelTweaks.setBorder(this);

    actionPanel = new JPanel(new FlowLayout(FlowLayout.LEADING, 2, 0));
    actionPanel.setBorder(BorderFactory.createEmptyBorder(2, 0, 2, 0));
    actionPanel.setOpaque(false);
    add("North", actionPanel);

    sortButton = new JToggleButton(new ToggleSortingAction());
    sortButton.setUI(new BlueishButtonUI());
    sortButton.setText(null);
    sortButton.setOpaque(false);
    actionPanel.add(sortButton);

    asCategoryButton = new JToggleButton(new ToggleModeAction());
    asCategoryButton.setUI(new BlueishButtonUI());
    asCategoryButton.setText(null);
    asCategoryButton.setOpaque(false);
    actionPanel.add(asCategoryButton);

    descriptionButton = new JToggleButton(new ToggleDescriptionAction());
    descriptionButton.setUI(new BlueishButtonUI());
    descriptionButton.setText(null);
    descriptionButton.setOpaque(false);
    actionPanel.add(descriptionButton);

    split = new JSplitPane(JSplitPane.VERTICAL_SPLIT);
    split.setBorder(null);
    split.setResizeWeight(1.0);
    split.setContinuousLayout(true);
    add("Center", split);

    tableScroll = new JScrollPane();
    tableScroll.setBorder(BorderFactory.createEmptyBorder());
    split.setTopComponent(tableScroll);

    descriptionPanel = new JEditorPane("text/html", "<html>");
    descriptionPanel.setBorder(BorderFactory.createEmptyBorder());
    descriptionPanel.setEditable(false);
    descriptionPanel.setBackground(UIManager.getColor("Panel.background"));
    LookAndFeelTweaks.htmlize(descriptionPanel);

    selectionListener = new SelectionListener();

    descriptionScrollPane = new JScrollPane(descriptionPanel);
    descriptionScrollPane.setBorder(LookAndFeelTweaks.addMargin(BorderFactory.createLineBorder(UIManager.getColor("controlDkShadow"))));
    descriptionScrollPane.getViewport().setBackground(descriptionPanel.getBackground());
    descriptionScrollPane.setMinimumSize(new Dimension(50, 50));
    split.setBottomComponent(descriptionScrollPane);

    // by default description is not visible, toolbar is visible.
    setDescriptionVisible(false);
    setToolBarVisible(true);
  }

  class SelectionListener implements ListSelectionListener {

    public void valueChanged(ListSelectionEvent e) {
      int row = table.getSelectedRow();
      Property prop = null;
      if (row >= 0 && table.getRowCount() > row)
        prop = model.getPropertySheetElement(row).getProperty();
      if (prop != null) {
        descriptionPanel.setText("<html>" + "<b>" + (prop.getDisplayName() == null ? "" : prop.getDisplayName()) + "</b><br>" +
            (prop.getShortDescription() == null ? "" : prop.getShortDescription()));
      } else {
        descriptionPanel.setText("<html>");
      }

      //position it at the top
      descriptionPanel.setCaretPosition(0);
    }
  }

  class ToggleModeAction extends AbstractAction {

    public ToggleModeAction() {
      super("toggle", IconPool.shared().get(PropertySheet.class.getResource("icons/category.gif")));
      putValue(Action.SHORT_DESCRIPTION,
          ResourceManager.get(PropertySheet.class).getString("PropertySheetPanel.category.shortDescription"));
    }

    public void actionPerformed(ActionEvent e) {
      if (asCategoryButton.isSelected()) {
        model.setMode(PropertySheet.VIEW_AS_CATEGORIES);
      } else {
        model.setMode(PropertySheet.VIEW_AS_FLAT_LIST);
      }
    }
  }

  class ToggleDescriptionAction extends AbstractAction {

    public ToggleDescriptionAction() {
      super("toggleDescription", IconPool.shared().get(PropertySheet.class.getResource("icons/description.gif")));
      putValue(Action.SHORT_DESCRIPTION,
          ResourceManager.get(PropertySheet.class).getString("PropertySheetPanel.tooltip.shortDescription"));
    }

    public void actionPerformed(ActionEvent e) {
      setDescriptionVisible(descriptionButton.isSelected());
    }
  }

  class ToggleSortingAction extends AbstractAction {

    public ToggleSortingAction() {
      super("toggleSorting", IconPool.shared().get(PropertySheet.class.getResource("icons/sort.gif")));
      putValue(Action.SHORT_DESCRIPTION, ResourceManager.get(PropertySheet.class).getString("PropertySheetPanel.sort.shortDescription"));
    }

    public void actionPerformed(ActionEvent e) {
      setSorting(sortButton.isSelected());
    }
  }

}