// AbstractPanel.java
//
// Created: September 2005
//
// This file is a component of the BioMoby project.
// Copyright Martin Senger (martin.senger@gmail.com).
//

package org.biomoby.service.dashboard;

import org.biomoby.shared.Utils;

import org.tulsoft.shared.PrefsUtils;
import org.tulsoft.tools.gui.SwingUtils;
import org.tulsoft.tools.gui.JTextFieldWithHistory;
import org.tulsoft.tools.gui.JFileChooserWithHistory;

import org.apache.commons.lang.StringUtils;
import org.apache.commons.io.FilenameUtils;

import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JSplitPane;
import javax.swing.Icon;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JTextArea;
import javax.swing.JOptionPane;
import javax.swing.JFileChooser;
import javax.swing.AbstractButton;
import javax.swing.BorderFactory;
import javax.swing.JMenuItem;
import javax.swing.JMenu;
import javax.swing.AbstractAction;
import javax.swing.JComponent;
import javax.swing.border.Border;
import javax.swing.border.CompoundBorder;
import javax.swing.border.TitledBorder;
import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.filechooser.FileFilter;

import java.awt.Component;
import java.awt.Container;
import java.awt.GridBagLayout;
import java.awt.GridBagConstraints;
import java.awt.Insets;
import java.awt.Font;
import java.awt.Color;
import java.awt.event.ActionListener;
import java.awt.event.ActionEvent;
import java.awt.event.ItemListener;
import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;

import java.io.File;
import java.io.IOException;
import java.util.prefs.Preferences;
import java.net.URL;

/**
 * Parent of all panels of a Dashboard. For Dashboard panels, it is
 * not mandatory to extends this class, it is just often convenient,
 * <p>
 *
 * @author <A HREF="mailto:martin.senger@gmail.com">Martin Senger</A>
 * @version $Id: AbstractPanel.java,v 1.27 2008/03/02 12:45:26 senger Exp $
 */

public abstract class AbstractPanel
    extends JPanel
    implements DashboardPanel, DashboardProperties {

    // copy here some often used constants
    protected static final int RELATIVE = GridBagConstraints.RELATIVE;
    protected static final int REMAINDER = GridBagConstraints.REMAINDER;
    protected static final int NONE = GridBagConstraints.NONE;
    protected static final int BOTH = GridBagConstraints.BOTH;
    protected static final int HORI = GridBagConstraints.HORIZONTAL;
    protected static final int VERT = GridBagConstraints.VERTICAL;
    protected static final int CENTER = GridBagConstraints.CENTER;
    protected static final int NORTH = GridBagConstraints.NORTH;
    protected static final int NEAST = GridBagConstraints.NORTHEAST;
    protected static final int EAST = GridBagConstraints.EAST;
    protected static final int SEAST = GridBagConstraints.SOUTHEAST;
    protected static final int SOUTH = GridBagConstraints.SOUTH;
    protected static final int SWEST = GridBagConstraints.SOUTHWEST;
    protected static final int WEST = GridBagConstraints.WEST;
    protected static final int NWEST = GridBagConstraints.NORTHWEST;

    // something need to be done only once - keep it here
    protected String panelIconFileName;
    protected Icon panelIcon;
    protected JComponent pComponent;

    // shared icons
    public static Icon confirmIcon;
    public static Icon warningIcon;
    public static Icon clearIcon;

    // re-use "style" components
    protected static final Insets BREATH_TOP = new Insets (10,0,0,0);
    protected static final Insets BREATH_TOP_LEFT = new Insets (10,10,0,0);
    protected static final Insets BREATH_LEFT = new Insets (0,10,0,0);
    protected static final Font MSG_AREA_FONT = new Font ("Courier", Font.PLAIN, 10);
    protected static final Font TITLE_FONT = new Font ("Serif", Font.BOLD, 20);
    protected static final Font FAT_BORDER_FONT = new Font ("Serif", Font.BOLD, 16);
    protected static final Color TITLE_FGCOLOR = new Color (12, 55, 241);

    // sharing events (across panels)
    protected PropertyChannel propertyChannel;

    /*********************************************************************
     * Default constructor (just for the sub-classes).
     ********************************************************************/
    protected AbstractPanel() {
	loadIcons();
    }

    /**************************************************************************
     *
     **************************************************************************/
    public String getHelp() {
	String panelClassName = Utils.simpleClassName (this.getClass().getName());
	String help = null;
	try {
	    help = Utils.readResource ("help" +
				       File.separator +
				       panelClassName + ".html",
				       this.getClass());
	    if (help != null)
		return help;
	} catch (IOException e) {
	}
	help = getDescription();
	if (help != null)
	    return help;

	return "";
    }

    /**************************************************************************
     *
     **************************************************************************/
    public URL getHelpURL() {
	String panelClassName = Utils.simpleClassName (this.getClass().getName());
	return Utils.getResourceURL ("help" +
				     File.separator +
				     panelClassName + ".html",
				     this.getClass());
    }

    /**************************************************************************
     *
     **************************************************************************/
    public String getDescription() {
	return "";
    }

    /**************************************************************************
     *
     **************************************************************************/
    public Icon getIcon() {
	if (panelIcon == null && panelIconFileName != null)
	    panelIcon = SwingUtils.createIcon (panelIconFileName, this);
	return panelIcon;
    }

    /**************************************************************************
     *
     **************************************************************************/
    public URL getIconURL() {
	return Utils.getResourceURL (panelIconFileName, this.getClass());
    }

    /**************************************************************************
     *
     **************************************************************************/
    public JLabel getTitle() {
	JLabel title = new JLabel (getName(), getIcon(), JLabel.CENTER);
	title.setFont (TITLE_FONT);
	title.setForeground (TITLE_FGCOLOR);
	return title;
    }

    /**************************************************************************
     *
     **************************************************************************/
    public boolean isMandatory() {
	return false;
    }

    /**************************************************************************
     *
     **************************************************************************/
    public boolean loadOnlyOnDemand() {
	return false;
    }

    /**************************************************************************
     * Remember the property channel.
     **************************************************************************/
    protected void setPropertyChannel (PropertyChannel propertyChannel) {
	this.propertyChannel = propertyChannel;
    }

    /**************************************************************************
     * Return a current (as posted in the propertyChannel) or a new
     * instance of a model accessing Biomoby registry. It any case,
     * give it our propertyChannel.
     *
     * Call this method only after a property channel has been set (by
     * method setPropertyChannel()).
     **************************************************************************/
    protected RegistryModel createRegistryModel() {
	synchronized (propertyChannel) {
	    RegistryModel rm;
	    if (propertyChannel.containsKey (DP_REGISTRY_MODEL)) {
		rm = (RegistryModel)propertyChannel.get (DP_REGISTRY_MODEL);
	    } else {
		rm = new RegistryModel();
		propertyChannel.put (DP_REGISTRY_MODEL, rm);
	    }
	    rm.setPropertyChannel (propertyChannel);
	    return rm;
	}
    }

    //
    // Abstract methods
    //

    /**************************************************************************
     *
     **************************************************************************/
    abstract public JComponent getComponent (PropertyChannel propertyChannel);

    /**************************************************************************
     *
     **************************************************************************/
    abstract public String getName();

    //
    // Few other methods
    //

    /*********************************************************************
     * Load shared icons.
     ********************************************************************/
    protected void loadIcons() {
	if (clearIcon == null)   clearIcon   = loadIcon ("images/smallClear.gif");
	if (warningIcon == null) warningIcon = loadIcon ("images/warningButton.gif");
	if (confirmIcon == null) confirmIcon = loadIcon ("images/confirmButton.gif");
    }

//     /*********************************************************************
//      * Make an icon-only button smaller (just around its icon).
//      ********************************************************************/
//     public static void compact (JButton button) {
// 	Insets margin = button.getMargin();
// 	margin.left = margin.right = margin.top;
// 	button.setMargin (margin);
//     }

    //
    // Methods to be used by sub-classes
    //

    /*********************************************************************
     * Load shared icon.
     ********************************************************************/
    public static Icon loadIcon (String path) {
	return SwingUtils.createIcon (path, Dashboard.class);
    }

    /*********************************************************************
     * Set a button to a common look-and-feel.
     ********************************************************************/
    protected static void commonButtonLookAndFeel (AbstractButton but) {
//         but.setMargin (new Insets (0,0,0,0));
//         but.setBorderPainted (false);
        but.setFocusPainted (false);
//         but.setContentAreaFilled (false);
    }

    /*********************************************************************
     * Create a button (with a unified style).
     ********************************************************************/
    protected static JButton createButton (String name,
					   String toolTipText,
					   int mnemonic,
					   ActionListener listener) {
	JButton button = new JButton (name);
 	commonButtonLookAndFeel (button);
        button.setToolTipText (toolTipText);
	if (mnemonic > 0)
	    button.setMnemonic (mnemonic);
        button.addActionListener (listener);
	return button;
    }

    /*********************************************************************
     * Create a check-box (with a unified style).
     ********************************************************************/
    protected static JCheckBox createCheckBox (String label,
					       boolean isSelected,
					       int mnemonic,
					       ItemListener listener) {
	JCheckBox box = new JCheckBox (label, isSelected);
	if (mnemonic > 0)
	    box.setMnemonic (mnemonic);
	if (listener != null)
	    box.addItemListener (listener);
        box.setFocusPainted (false);
	return box;
    }

    /*********************************************************************
     * Create a horizontal panel with given buttons.
     ********************************************************************/
    protected static JPanel createButtonPanel (JButton[] buttons) {

	JPanel buttonPanel = new JPanel();
	buttonPanel.setLayout (new BoxLayout (buttonPanel, BoxLayout.LINE_AXIS));
	buttonPanel.setBorder (BorderFactory.createEmptyBorder (10, 0, 0, 10));
	buttonPanel.add (Box.createHorizontalGlue());
	for (int i = 0; i < buttons.length; i++)
	    buttonPanel.add (buttons[i]);
	return buttonPanel;
    }

    /*********************************************************************
     * Create a titled panel using the given title and GridBagLayout.
     ********************************************************************/
    protected static JPanel createTitledPanel (String title) {
	JPanel titledPanel = new JPanel (new GridBagLayout());
	Border blackline = BorderFactory.createLineBorder (Color.black);
	CompoundBorder compoundBorder =
	    BorderFactory.createCompoundBorder (BorderFactory.createTitledBorder (blackline, title),
						BorderFactory.createEmptyBorder (5, 5, 5, 5));
	titledPanel.setBorder (compoundBorder);
	return titledPanel;
    }

    /*********************************************************************
     * Create a impressive (fatter) colored border - with given title
     * in the center.
     ********************************************************************/
    protected static Border createFatBorder (String title, Color color) {
	Border colorline =
	    BorderFactory.createLineBorder (color, 3);
	TitledBorder titledBorder =
	    BorderFactory.createTitledBorder (colorline, title);
	titledBorder.setTitleJustification (TitledBorder.CENTER);
	titledBorder.setTitleFont (FAT_BORDER_FONT);
	return BorderFactory.createCompoundBorder
	    (titledBorder,
	     BorderFactory.createEmptyBorder (5, 5, 5, 5));
    }

    /*********************************************************************
     * Create a panel with given text field and with a directory/file
     * browser button attached.
     ********************************************************************/
    protected JFileChooserWithHistory createFileSelector (String chooserTitle,
							  String approveButtonText,
							  String defaultValue,
							  String preferenceNode,
							  String eventName) {
	JFileChooserWithHistory chooser =
	    new JFileChooserWithHistory (null, this, preferenceNode);

	JFileChooser ch = chooser.getFileChooser();
	ch.setApproveButtonText (approveButtonText);
	ch.setDialogTitle (chooserTitle);

 	if (StringUtils.isNotBlank (defaultValue) &&
 	    chooser.getSelectedFile() == null)
 	    chooser.setSelectedFile (new File (defaultValue));

	if (eventName != null) {
	    JTextFieldWithHistory textField = chooser.getTextField();
	    textField.addActionListener (getTextFieldListener (eventName));
	    String text = textField.getText();
	    if (StringUtils.isNotBlank (text))
		propertyChannel.put (eventName, text);
	}

	return chooser;
    }

    /*********************************************************************
     * Create a text field (possibly with an 'initValue') with its
     * history taken from given 'preferenceNode'. <p>
     *
     * If 'initValue' is an empty string, the text field starts with
     * an empty initial value. If the 'initValue' is null, the text
     * field starts filled with the last time used value (stored in
     * and taken from the 'preference node'). <p>
     *
     * If 'evenName' is not null (which means that the contents of
     * this text field may be of interest of someone outside), add an
     * action event that will update the shared property storage
     * (which is in a global variable 'propertyChannel') when the text
     * field changes its value. Also in this case, send the initial
     * value to the same shared property storage/channel.
     ********************************************************************/
    protected JTextFieldWithHistory createText (String initValue,
						String preferenceNode,
						String eventName) {
	JTextFieldWithHistory textField =
	    new JTextFieldWithHistory (initValue,
				       this,
				       preferenceNode);
	if (eventName != null) {
	    textField.addActionListener (getTextFieldListener (eventName));
	    String text = textField.getText();
	    if (StringUtils.isNotBlank (text))
		propertyChannel.put (eventName, text);
	}

	return textField;
    }

    /*********************************************************************
     * 
     ********************************************************************/
    private ActionListener getTextFieldListener (String eventName) {
	final String name = eventName;
	return new ActionListener() {
		public void actionPerformed (ActionEvent e) {
		    String contents =  ((JTextFieldWithHistory)e.getSource()).getText();
		    propertyChannel.put (name,
					 contents == null ? "" : contents);
		}
	    };
    }

    /*********************************************************************
     * 
     ********************************************************************/
    protected JPanel createCustomTextArea (String title,
					   String initValue,
					   String preferenceKey,
					   String eventName) {
	return createCustomTextArea (title, initValue, preferenceKey, eventName,
				     new JTextArea());
    }

    /*********************************************************************
     * 
     ********************************************************************/
    protected JPanel createCustomTextArea (String title,
					   String initValue,
					   String preferenceKey,
					   String eventName,
					   JTextArea myArea) {
	JPanel p = new JPanel (new GridBagLayout());

	// main label
   	JLabel label = new JLabel (title);

	// text area
	final JTextArea area = myArea;
	area.setEditable (true);
	if (initValue == null)
	    area.setText (preferenceKey == null ? "" : getPrefValue (preferenceKey, ""));
	else
	    area.setText (initValue);
	area.setCaretPosition (0);
	area.setLineWrap (true);
	area.setWrapStyleWord (true);

	if (eventName != null) {
	    final String eName = eventName;
	    if (preferenceKey == null) {
		area.addFocusListener (new FocusListener() {
			public void focusGained (FocusEvent e) {}
			public void focusLost (FocusEvent e) {
			    String contents =  ((JTextArea)e.getSource()).getText();
			    propertyChannel.put (eName, contents);
			}
		    });
	    } else {
		final String pKey = preferenceKey;
		area.addFocusListener (new FocusListener() {
			public void focusGained (FocusEvent e) {}
			public void focusLost (FocusEvent e) {
			    String contents =  ((JTextArea)e.getSource()).getText();
			    propertyChannel.put (eName, contents);
			    setPrefValue (pKey, contents);
			}
		    });
	    }
	    // propagate also the initial value (unless it is empty)
	    String text = area.getText();
	    if (StringUtils.isNotBlank (text))
		propertyChannel.put (eventName, text);
	}

	// reset/clear button
	JButton clearButton = new JButton (clearIcon);
        clearButton.setFocusPainted (false);
        clearButton.setMargin (new Insets (0,0,0,0));
        clearButton.setContentAreaFilled (false);
        clearButton.setToolTipText ("Clear " + title.toLowerCase() + " text area");
	clearButton.addActionListener (new ActionListener() {
		public void actionPerformed (ActionEvent e) {
		    area.requestFocusInWindow();
		    area.setText ("");
		}
	    });

	// put it together
 	SwingUtils.addComponent (p, label,                  0, 0, 1, 1, NONE, NWEST, 0.0, 0.0);
 	SwingUtils.addComponent (p, clearButton,            1, 0, 1, 1, NONE, NEAST, 0.0, 0.0);
 	SwingUtils.addComponent (p, new JScrollPane (area), 0, 1, 2, 1, BOTH, NWEST, 1.0, 1.0);
	return p;
    }

    /*********************************************************************
     * Create a menu item.
     ********************************************************************/
    public static JMenuItem createMenuItem (AbstractAction action,
					    String actionCommand) {
	JMenuItem mi = new JMenuItem (action);
	mi.setActionCommand (actionCommand);
	return mi;
    }

    /*********************************************************************
     * Create a menu item.
     ********************************************************************/
    public static JMenuItem createMenuItem (AbstractAction action,
					    String actionCommand,
					    int mnemonic,
					    Icon icon,
					    Icon disabledIcon) {
	JMenuItem mi = createMenuItem (action, actionCommand);
	mi.setIcon (icon);
	mi.setDisabledIcon (disabledIcon);
        if (mnemonic > 0)
	    mi.setMnemonic (mnemonic);
	return mi;
    }

    /*********************************************************************
     * Create a submenu.
     ********************************************************************/
    public static JMenu createMenu (String name,
				    int mnemonic,
				    Icon icon,
				    Icon disabledIcon) {
	JMenu m = new JMenu(name);
	m.setIcon (icon);
	m.setDisabledIcon (disabledIcon);
        if (mnemonic > 0)
	    m.setMnemonic (mnemonic);
	return m;
    }

    /*********************************************************************
     * Enable/disable a menu item (indicated by an 'actionCommand') in
     * the given container.
     ********************************************************************/
    public static void setEnabledMenuItem (Container container,
					   String actionCommand,
					   boolean enabled) {
	Component[] components;
	if (container instanceof JMenu)
	    components = ((JMenu)container).getMenuComponents();
	else
	    components = container.getComponents();
	for (int i = 0; i < components.length; i++) {
	    if (components[i] instanceof JMenu) {
		setEnabledMenuItem ((JMenu)components[i], actionCommand, enabled);
	    } else if ( components[i] instanceof JMenuItem) {
		 if (actionCommand.equals (((JMenuItem)components[i]).getActionCommand()) ) {
		     ((JMenuItem)components[i]).setEnabled (enabled);
		     return;
		 }
	    }
	}
    }

    /*********************************************************************
     * Split two components horizontaly with given weight. Add some
     * common style.
     ********************************************************************/
    protected JSplitPane hSplit (Component a, Component b, double weight) {
	JSplitPane split = new JSplitPane (JSplitPane.HORIZONTAL_SPLIT,
					   a, b);
 	split.setResizeWeight (weight);
	split.setDividerLocation (weight);
	split.setContinuousLayout (true);
	split.setOneTouchExpandable (true);
	return split;
    }

    /*********************************************************************
     * Split two components vertically with given weight. Add some
     * common style.
     ********************************************************************/
    protected JSplitPane vSplit (Component a, Component b, double weight) {
	JSplitPane split = new JSplitPane (JSplitPane.VERTICAL_SPLIT,
					   a, b);
 	split.setResizeWeight (weight);
 	split.setDividerLocation (weight);
	split.setContinuousLayout (true);
	split.setOneTouchExpandable (true);
	return split;
    }

    /*********************************************************************
     * 
     ********************************************************************/
    protected String getPrefValue (String key,
				   String defaultValue) {
	if (key == null) return defaultValue;
	Preferences node = PrefsUtils.getNode (this.getClass());
 	return showNewlines (node.get (key, defaultValue));
    }

    protected boolean getPrefValue (String key,
				    boolean defaultValue) {
	if (key == null) return defaultValue;
	Preferences node = PrefsUtils.getNode (this.getClass());
	return node.getBoolean (key, defaultValue);
    }

    /*********************************************************************
     * 
     ********************************************************************/
    protected void setPrefValue (String key,
				 String value) {
	if (key != null) {
	    Preferences node = PrefsUtils.getNode (this.getClass());
	    node.put (key, hideNewlines (value));
	}
    }

    protected void setPrefValue (String key,
				 boolean value) {
	if (key != null) {
	    Preferences node = PrefsUtils.getNode (this.getClass());
	    node.putBoolean (key, value);
	}
    }

    /*********************************************************************
     * Sorry for this hack, but I do not know better...
     *
     * Problem is that attribute values (preferences) ignore
     * newlines. I wanted to replace them with something like '&#10;'
     * but this did not work because the ampersand was escaped (as it
     * should be) to '&amp;'. So I have to "invent" my own escaping by
     * introducing MRVAJS.
     ********************************************************************/
    static final private String MRVAJS = "#MRVAJS#";
    private static String hideNewlines (String value) {
	return value.replaceAll ("\n", MRVAJS);
    }
    private static String showNewlines (String value) {
	return value.replaceAll (MRVAJS, "\n");
    }

    /*********************************************************************
     * Return true if confirmation dialog passed.
     ********************************************************************/
    public static boolean confirm (Object msg) {
        return SwingUtils.confirm (null, msg, confirmIcon);
    }

    /*********************************************************************
     * Return true if confirmation dialog passed.
     ********************************************************************/
    public static boolean confirm (Component parent, Object msg) {
        return SwingUtils.confirm (parent, msg, confirmIcon);
    }

    /*********************************************************************
     * Display an error message.
     ********************************************************************/
    public static void error (Object msg) {
	JOptionPane.showMessageDialog (null, msg, "Error message",
				       JOptionPane.PLAIN_MESSAGE, warningIcon);
    }

    /*********************************************************************
     * Display an error message. Construct the message from
     * 'prologue', some intermediate title, and a text area filled
     * from the exception 'e'.
     ********************************************************************/
    public static void error (String prologue, Exception e) {

	// slightly format message prologue
	// TBD: HTML escape
	prologue = prologue + "The actual error is:\n\n";
	if (! prologue.startsWith ("<html")) {
	    prologue = "<html>" + prologue + "</html>";
	    prologue = prologue.replaceAll ("\n", "<br>\n");
	}

	// slightly format the error message
	// TBD: make it (or part of it) red
	String msg = e.getMessage();

	JTextArea area = new JTextArea (15, 50);
	area.setEditable (false);
	area.setFont (MSG_AREA_FONT);	
	area.setText (msg);
	area.setCaretPosition (0);

	JPanel p = new JPanel (new GridBagLayout());
 	SwingUtils.addComponent (p, new JLabel (prologue),  0, 0, 1, 1, NONE, NWEST, 0.0, 0.0);
 	SwingUtils.addComponent (p, new JScrollPane (area), 0, 1, 1, 1, BOTH, NWEST, 1.0, 1.0);
	error (p);
    }

    /*********************************************************************
     * Accept all directories and all XML files.
     ********************************************************************/
    protected static FileFilter getXMLFilter() {
	return new FileFilter() {
		public boolean accept (File f) {
		    if (f.isDirectory()) return true;
		    String extension = FilenameUtils.getExtension (f.getName());
		    return ("xml".equalsIgnoreCase (extension));
		}
		public String getDescription() {
		    return "XML files";
		}
	    };
    }
}
