// RegistryPanel.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.tulsoft.tools.gui.SwingUtils;
import org.tulsoft.tools.gui.JTextFieldWithHistory;
import org.tulsoft.tools.gui.JFileChooserWithHistory;

import org.biomoby.shared.MobyException;
import org.biomoby.registry.meta.Registries;
import org.biomoby.registry.meta.RegistriesList;
import org.biomoby.registry.meta.Registry;

import org.apache.commons.io.FilenameUtils;

import javax.swing.JPanel;
import javax.swing.JLabel;
import javax.swing.Icon;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JOptionPane;
import javax.swing.JFileChooser;
import javax.swing.JSplitPane;
import javax.swing.JComponent;
import javax.swing.JComboBox;

import java.awt.GridBagLayout;
import java.awt.event.KeyEvent;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.ItemEvent;
import java.awt.event.ItemListener;

import java.io.File;
import java.util.List;
import java.util.ArrayList;

/**
 * A panel displaying contents of a Biomoby registry. It also select
 * what Biomoby registry to work with, and what cache directory to
 * use. <p>
 *
 * @author <A HREF="mailto:martin.senger@gmail.com">Martin Senger</A>
 * @version $Id: RegistryPanel.java,v 1.31 2008/11/18 06:40:11 senger Exp $
 */

public class RegistryPanel
    extends AbstractPanel {

    private static org.apache.commons.logging.Log log =
       org.apache.commons.logging.LogFactory.getLog (RegistryPanel.class);

    // names of user preferences keys
    static final String USE_CACHE = "use-cache";
    static final String SHOW_REG_INFO = "show-reg-info";

    // associated model working behind the scene
    RegistryModel registryModel;

    // components that are used from more methods
    JTextFieldWithHistory registryURL;
    JTextFieldWithHistory registryNS;
    JFileChooserWithHistory cacheDir;
    JLabel labelCacheDir;
    CommonConsole console;

    JCheckBox bServices, bDataTypes, bNamespaces, bServiceTypes, bAll;
    JButton infoButton, updateButton, eraseButton;

    DataTypesBoard dataTypesBoard;
    ServiceTypesBoard serviceTypesBoard;
    NamespacesBoard namespacesBoard;
    ServicesBoard servicesBoard;

    // shared icons
    protected static Icon defaultsIcon;
    protected static Icon reloadIcon;
    protected static Icon infoIcon, updateIcon, eraseIcon;

    /*********************************************************************
     * Default constructor.
     ********************************************************************/
    public RegistryPanel() {
	super();
	panelIconFileName = "images/registry.gif";
    }

    /*********************************************************************
     * Load shared icons.
     ********************************************************************/
    protected void loadIcons() {
	super.loadIcons();
	if (defaultsIcon == null) defaultsIcon = loadIcon ("images/smallUndo.gif");
	if (reloadIcon == null) reloadIcon = loadIcon ("images/smallReload.gif");
	if (infoIcon == null) infoIcon = loadIcon ("images/smallInfo.gif");
	if (updateIcon == null) updateIcon = loadIcon ("images/smallSynch.gif");
	if (eraseIcon == null) eraseIcon = loadIcon ("images/smallTrash.gif");
    }
	
    /**************************************************************************
     * Return a temporary directory that is different for each user
     * (so no just /tmp).
     **************************************************************************/
    protected String getUserTmpDir() {
	String standardTmpDir = System.getProperty ("java.io.tmpdir");
	String userName = System.getProperty ("user.name");
	if (standardTmpDir.indexOf (userName) > -1)
	    return standardTmpDir;
	File userTmpDir = new File (standardTmpDir, userName);
	if (userTmpDir.exists())
	    return userTmpDir.getAbsolutePath();
	if (userTmpDir.mkdirs())
	    return userTmpDir.getAbsolutePath();
	return standardTmpDir;
    }

    /**************************************************************************
     *
     **************************************************************************/
    public JComponent getComponent (PropertyChannel propertyChannel) {
 	setPropertyChannel (propertyChannel);
	registryModel = createRegistryModel();

	if (pComponent != null) return pComponent;
	pComponent = new JPanel (new GridBagLayout(), true);

	// registry and cache locations (this must be created before
	// creating various ontology trees because the trees will need
	// to know registry and cache locations)
	JPanel regLocation = getRegistryLocation();
	JPanel cacheLocation = getCacheLocation();

	// console panel
	console = new CommonConsole();
	console.setAppendMode (false);

	// ontology trees
	dataTypesBoard = new DataTypesBoard (registryModel,
					     console,
					     propertyChannel);
// 	dataTypesBoard.updateTree (CommonTree.SORTED_BY_NAME);
// 	log.debug ("Data Types tree update started");
	serviceTypesBoard = new ServiceTypesBoard (registryModel,
						   console,
						   propertyChannel);
// 	serviceTypesBoard.updateTree (CommonTree.SORTED_BY_NAME);
// 	log.debug ("Service Types tree update started");
	namespacesBoard = new NamespacesBoard (registryModel,
					       console,
					       propertyChannel);
// 	namespacesBoard.updateTree (CommonTree.SORTED_BY_NAME);
// 	log.debug ("Namespaces tree update started");
	servicesBoard = new ServicesBoard (registryModel,
					   console,
					   propertyChannel);
// 	servicesBoard.updateTree (CommonTree.SORTED_BY_NAME);
// 	log.debug ("Services tree update started");

	dataTypesBoard.updateTree (CommonTree.SORTED_BY_NAME);
	log.debug ("Data Types tree update started");
	serviceTypesBoard.updateTree (CommonTree.SORTED_BY_NAME);
	log.debug ("Service Types tree update started");
	namespacesBoard.updateTree (CommonTree.SORTED_BY_NAME);
	log.debug ("Namespaces tree update started");
	servicesBoard.updateTree (CommonTree.SORTED_BY_NAME);
	log.debug ("Services tree update started");

	// split it into moving panels
	JSplitPane split1 = hSplit (servicesBoard, dataTypesBoard, 0.5);
	JSplitPane split2 = hSplit (serviceTypesBoard, namespacesBoard, 0.5);
	JSplitPane split3 = hSplit (split1, split2, 0.5);
	JSplitPane split4 = vSplit (split3, console, 0.5);

	// put all together
 	SwingUtils.addComponent (pComponent, split4,        0, 0, 1, 2, BOTH, NWEST, 1.0, 1.0);
  	SwingUtils.addComponent (pComponent, regLocation,   1, 0, 1, 1, HORI, NWEST, 0.0, 0.0);
  	SwingUtils.addComponent (pComponent, cacheLocation, 1, 1, 1, 1, HORI, NWEST, 0.0, 0.0);

	return pComponent;
    }

    /**************************************************************************
     * It updates all lists. Each tree is responsible to reload itself
     * in a separate thread.
     **************************************************************************/
    protected void onReloadAll() {
	dataTypesBoard.updateTree (CommonTree.SORTED_AS_PREVIOUSLY);
	serviceTypesBoard.updateTree (CommonTree.SORTED_AS_PREVIOUSLY);
	namespacesBoard.updateTree (CommonTree.SORTED_AS_PREVIOUSLY);
	servicesBoard.updateTree (CommonTree.SORTED_AS_PREVIOUSLY);
    }

    /**************************************************************************
     * Replace text fields that define location of a Biomoby registry
     * with a default location.
     **************************************************************************/
    protected void onDefaults() {
	registryURL.setText (registryModel.getDefaultRegistryEndpoint());
	registryNS.setText (registryModel.getDefaultRegistryNamespace());
    }

    /**************************************************************************
     *
     **************************************************************************/
    private JPanel createCacheDialog (String introText) {
	JPanel p = new JPanel (new GridBagLayout());
	JLabel start = new JLabel (introText);

	bServices = createCheckBox ("Services", false, KeyEvent.VK_S, null);
	bDataTypes = createCheckBox ("Data types", false, KeyEvent.VK_D, null);
	bNamespaces = createCheckBox ("Namespaces", false, KeyEvent.VK_N, null);
	bServiceTypes = createCheckBox ("Service types", false, KeyEvent.VK_T, null);
	bAll = createCheckBox ("All", false, KeyEvent.VK_A,
			       new ItemListener() {
				   public void itemStateChanged (ItemEvent e) {
				       boolean enabled = (e.getStateChange() != ItemEvent.SELECTED);
				       bServices.setEnabled (enabled);
				       bDataTypes.setEnabled (enabled);
				       bNamespaces.setEnabled (enabled);
				       bServiceTypes.setEnabled (enabled);
				   }
			       });
	// put it together
 	SwingUtils.addComponent (p, start,         0, 0, 1, 1, NONE, NWEST, 0.0, 0.0);
 	SwingUtils.addComponent (p, bServices,     0, 1, 1, 1, NONE, NWEST, 0.0, 0.0, BREATH_TOP);
 	SwingUtils.addComponent (p, bDataTypes,    0, 2, 1, 1, NONE, NWEST, 0.0, 0.0);
 	SwingUtils.addComponent (p, bNamespaces,   0, 3, 1, 1, NONE, NWEST, 0.0, 0.0);
 	SwingUtils.addComponent (p, bServiceTypes, 0, 4, 1, 1, NONE, NWEST, 0.0, 0.0);
 	SwingUtils.addComponent (p, bAll,          0, 5, 1, 1, NONE, NWEST, 0.0, 0.0, BREATH_TOP);
	return p;
    }

    /**************************************************************************
     *
     **************************************************************************/
    protected void onCacheInfo() {

	final JLabel contents = new JLabel();
	final String[] buttons = new String[] { "Copy to console", "Done"};
	propertyChannel.fire (DP_STATUS_MSG, "Retrieving cache info...");
	infoButton.setEnabled (false);

	final SwingWorker worker = new SwingWorker() {
		MobyException exception = null;
		String info = null;
		public Object construct() {
		    try {
			info = registryModel.getCacheInfoFormatted();
		    } catch (MobyException e) {
			exception = e;
		    }
		    return null;  // not used here
		}

		// runs on the event-dispatching thread.
		public void finished() {
		    if (exception == null)
 			contents.setText ("<html><pre>" + info + "</pre>");
		    else
			contents.setText ("<html>Sorry, I could not retrieve any info...<br>" +
					  "I think that the problem is actually here:<p><pre>" +
					  exception.getMessage() +
					  "</pre>");
		    if (JOptionPane.showOptionDialog (null, contents,
						      "Info on local cache",
						      JOptionPane.YES_NO_OPTION,
						      JOptionPane.PLAIN_MESSAGE,
						      confirmIcon,
						      buttons,
						      null) == 0)
			console.setText (info);
		    propertyChannel.fire (DP_STATUS_MSG, "Done");
		    infoButton.setEnabled (true);
		}
	    };
	worker.start(); 
    }

    /**************************************************************************
     *
     **************************************************************************/
    protected void onCacheErase() {
    }

    final static String UPDATE_LABEL =
    "<html>Update or reload your local cache<br>" +
    "for the entities selected below.<p><p>" +

    "<font color='red'>Update</font> is faster than reload but<br>" +
    "it may not reflect changes in the<br>" +
    "contents of the entities.<p><p>" +

    "<font color='red'>Reload</font> is slower than update but it<br>" +
    "guarantees that the full contents<br>" +
    "of all cached entities is up to date.<p>";

    /**************************************************************************
     *
     **************************************************************************/
    protected void onCacheUpdate() {
	JPanel p = createCacheDialog (UPDATE_LABEL);
	String[] buttons = new String[] { "Update", "Reload", "Cancel"};
	int selected =
	    JOptionPane.showOptionDialog (null, p,
					  "Update/Reload local cache",
					  JOptionPane.YES_NO_OPTION,
					  JOptionPane.QUESTION_MESSAGE,
					  confirmIcon,
					  buttons,
					  null);
	boolean toReload;
	if (selected == 0)
	    toReload = false; // 'update' selected
	else if (selected == 1)
	    toReload = true;  // 'reload' selected
	else
	    return;           // cancelled

	// update each cache part in a separate (and concurrent) thread
	boolean all = bAll.isSelected();
	if (all || bServices.isSelected())
	    updateCache (RegistryModel.PART_SERVICES, toReload);
	if (all || bDataTypes.isSelected())
	    updateCache (RegistryModel.PART_DATA_TYPES, toReload);
	if (all || bNamespaces.isSelected())
	    updateCache (RegistryModel.PART_NAMESPACES, toReload);
	if (all || bServiceTypes.isSelected())
	    updateCache (RegistryModel.PART_SERVICE_TYPES, toReload);
    }
	    
    /**************************************************************************
     *
     **************************************************************************/
    private void updateCache (int cachePart, boolean reload) {
	final int myCachePart = cachePart;
	final boolean myReload = reload;
	final SwingWorker worker = new SwingWorker() {
		MobyException exception = null;
		public Object construct() {
		    try {
			switch (myCachePart) {
			case RegistryModel.PART_SERVICES:
			    if (myReload)
				registryModel.reloadServicesCache();
			    else
				registryModel.updateServicesCache();
			    break;
			case RegistryModel.PART_DATA_TYPES:
			    if (myReload)
				registryModel.reloadDataTypesCache();
			    else
				registryModel.updateDataTypesCache();
			    break;
			case RegistryModel.PART_SERVICE_TYPES:
			    if (myReload)
				registryModel.reloadServiceTypesCache();
			    else
				registryModel.updateServiceTypesCache();
			    break;
			case RegistryModel.PART_NAMESPACES:
			    if (myReload)
				registryModel.reloadNamespacesCache();
			    else
				registryModel.updateNamespacesCache();
			    break;
			}
		    } catch (MobyException e) {
			exception = e;
		    }
		    return null;  // not used here
		}

		// runs on the event-dispatching thread.
		public void finished() {
		    if (log.isDebugEnabled())
			log.debug ( (myReload ? "Reload of " : "Update of part ") + myCachePart + " finished");
		    if (exception != null)
			error ("An error occured when filling/updating the cache.\n\n",
			       exception);
		}

	    };
	worker.start(); 

    }

    /**************************************************************************
     *
     **************************************************************************/
    protected void onUseCache (boolean enabled) {
	cacheDir.setEnabled (enabled);
	labelCacheDir.setEnabled (enabled);
	infoButton.setEnabled (enabled);
	updateButton.setEnabled (enabled);
	setPrefValue (USE_CACHE, enabled);
	propertyChannel.put (DP_USE_CACHE, new Boolean (enabled).toString());
    }

    /*********************************************************************
     *
     ********************************************************************/
    protected JPanel getKnownRegistries() {
   	JLabel labelReg = new JLabel ("Known registries");

	boolean showingReg = getPrefValue (SHOW_REG_INFO, false);
	JCheckBox showReg =
	    createCheckBox ("Show info", showingReg, -1,
			    new ItemListener() {
				public void itemStateChanged (ItemEvent e) {
				    boolean enabled = (e.getStateChange() == ItemEvent.SELECTED);
				    setPrefValue (SHOW_REG_INFO, enabled);
				    propertyChannel.put (DP_REG_INFO,
							 new Boolean (enabled).toString());
				}
			    });
	propertyChannel.put (DP_REG_INFO, new Boolean (showingReg).toString());
        showReg.setToolTipText ("Each time you select a registry, an info appears in console window");

	final Registries regs = RegistriesList.getInstance();
        JComboBox regList = new JComboBox (getOnlyWantedRegistries (regs.list()));
        regList.setToolTipText ("A selection will fill text fields below");
	final String defaultRegistry = getDefaultRegistrySynonym();
	regList.setSelectedItem (getPrefValue (DP_REGISTRY_SYNONYM, defaultRegistry));
	regList.addActionListener (new ActionListener() {
		public void actionPerformed (ActionEvent e) {
		    String contents =  (String)((JComboBox)e.getSource()).getSelectedItem();
		    setPrefValue (DP_REGISTRY_SYNONYM, contents);
		    Registry theReg = null;
		    try {
			theReg = regs.get (contents);
		    } catch (MobyException ee) {
			try {
			    theReg = regs.get (defaultRegistry);
			} catch (MobyException ee2) {
			    log.error ("List of registries does not contain the default registry.");
			}
		    }
		    if (theReg != null) {
			boolean show = propertyChannel.getBoolean (DP_REG_INFO, false);
			if (show)
			    console.setText (theReg.toString());
			registryURL.setText (theReg.getEndpoint());
			registryNS.setText (theReg.getNamespace());

		    }
		}
	    });

	JPanel p = new JPanel (new GridBagLayout());
 	SwingUtils.addComponent (p, labelReg, 0, 0, 1, 1, NONE, NWEST, 0.0, 0.0);
  	SwingUtils.addComponent (p, regList,  0, 1, 1, 1, HORI, NWEST, 1.0, 0.0);
  	SwingUtils.addComponent (p, showReg,  1, 1, 1, 1, NONE, NWEST, 0.0, 0.0);
	return p;
    }

    /**************************************************************************
     * Filter the given list of known registry by an optional
     * properties. Return the filtered result, or the same list if
     * there is no relevant property. Check if the wanted registries
     * are also registered ones and ignore (with warning) those that
     * are not.
     **************************************************************************/
    protected static String[] getOnlyWantedRegistries (String[] regs) {
	String[] wantedRegs = DashboardConfig.getStrings (DP_WANTED_REGISTRIES, null);

 	if (wantedRegs.length == 0)
 	    return regs;

	// check if all of the wanted registries are also known ones
	List<String> registeredAndWanted = new ArrayList<String>();
	for (String wantedReg: wantedRegs) {
	    boolean found = false;
	    for (String knownReg: regs) {
		if (wantedReg.equals (knownReg)) {
		    found = true;
		    break;
		}
	    }
	    if (found) {
		registeredAndWanted.add (wantedReg);
	    } else {
		log.warn ("An unknown registry synonym found in the property " +
			  DP_WANTED_REGISTRIES + ": " + wantedReg);
	    }
	}
	if (registeredAndWanted.size() > 0) {
	    return registeredAndWanted.toArray (new String[] {});
	} else {
	    return regs;
	}
    }

    /**************************************************************************
     * Return a synonym of the default registry. It is retrieved (in
     * this order):
     *
     * - from the property DP_DEFAULT_REGISTRY;
     * - from Registries.DEFAULT_REGISTRY_SYNONYM.
     *
     * Log warning if the returned default registry is not in the list
     * of wanted registries.
     **************************************************************************/
    protected static String getDefaultRegistrySynonym() {

	String defaultReg = DashboardConfig.getString (DP_DEFAULT_REGISTRY, null);
	if (defaultReg == null)
	    defaultReg = Registries.DEFAULT_REGISTRY_SYNONYM;

	String[] wantedRegs =
	    getOnlyWantedRegistries (RegistriesList.getInstance().list());
	for (String wantedReg: wantedRegs) {
	    if (defaultReg.equals (wantedReg))
		return defaultReg;
	}
	log.warn ("Default registry '" + defaultReg + "' is not found in wanted registries.");
	return defaultReg;
    }

    /**************************************************************************
     * Panel for registry.
     **************************************************************************/
    protected JPanel getRegistryLocation() {
	JPanel regs = getKnownRegistries();
   	JLabel labelRegistryURL = new JLabel("Endpoint");
	registryURL = createText (null, "registryEndpoint", DP_REGISTRY_ENDPOINT);
   	JLabel labelRegistryNS = new JLabel("Namespace (URI)");
	registryNS = createText (null, "registryNamespace", DP_REGISTRY_NAMESPACE);
	JButton reloadAllButton =
	    createButton (" Reload all lists ",
			  "Reload all ontology trees from a Biomoby registry, or from the cache",
			  KeyEvent.VK_R,
			  new ActionListener() {
			      public void actionPerformed (ActionEvent e) {
				  onReloadAll();
			      }
			  });
	reloadAllButton.setIcon (reloadIcon);
	JButton defaultsButton =
	    createButton (" Restore defaults ",
			  "Fill the text fields above with the default values",
			  KeyEvent.VK_D,
			  new ActionListener() {
			      public void actionPerformed (ActionEvent e) {
				  onDefaults();
			      }
			  });
	defaultsButton.setIcon (defaultsIcon);

	JPanel buttonPanel = createButtonPanel (new JButton[] { reloadAllButton,
								defaultsButton });
	JPanel rLocation = createTitledPanel ("Biomoby registry location");
 	SwingUtils.addComponent (rLocation, regs,             0, 0, 1, 1, HORI, NWEST, 1.0, 0.0);
 	SwingUtils.addComponent (rLocation, labelRegistryURL, 0, 1, 1, 1, NONE, NWEST, 0.0, 0.0, BREATH_TOP);
 	SwingUtils.addComponent (rLocation, registryURL,      0, 2, 1, 1, HORI, NWEST, 1.0, 0.0);
 	SwingUtils.addComponent (rLocation, labelRegistryNS,  0, 3, 1, 1, NONE, NWEST, 0.0, 0.0, BREATH_TOP);
 	SwingUtils.addComponent (rLocation, registryNS,       0, 4, 1, 1, HORI, NWEST, 1.0, 0.0);
 	SwingUtils.addComponent (rLocation, buttonPanel,      0, 5, 1, 1, NONE, SWEST, 0.0, 0.0);

	return rLocation;
    }

    /**************************************************************************
     * Panel for local cache.
     **************************************************************************/
    protected JPanel getCacheLocation() {
	boolean usingCache = getPrefValue (USE_CACHE, true);
	JCheckBox useCache =
	    createCheckBox ("Use local cache", usingCache, KeyEvent.VK_C,
			    new ItemListener() {
				public void itemStateChanged (ItemEvent e) {
				    onUseCache (e.getStateChange() == ItemEvent.SELECTED);
				}
			    });
	propertyChannel.put (DP_USE_CACHE, new Boolean (usingCache).toString());

   	labelCacheDir = new JLabel("Cache directory");
	cacheDir = createFileSelector ("Select directory for/with local cache",
				       "Select",
				       FilenameUtils.separatorsToSystem
				       (DashboardConfig.getString (DP_REGISTRY_CACHE_DIR,
								   getUserTmpDir())),
				       "cacheDirectory",
				       DP_CACHE_DIR);
	cacheDir.getFileChooser().setFileSelectionMode (JFileChooser.DIRECTORIES_ONLY);

	infoButton =
	    createButton (" Info ",
			  "Show current information about the local cache",
			  KeyEvent.VK_I,
			  new ActionListener() {
			      public void actionPerformed (ActionEvent e) {
				  onCacheInfo();
			      }
			  });
 	infoButton.setIcon (infoIcon);

	updateButton =
	    createButton (" Update ",
			  "Update local cache from Biomoby registry",
			  KeyEvent.VK_U,
			  new ActionListener() {
			      public void actionPerformed (ActionEvent e) {
				  onCacheUpdate();
			      }
			  });
	updateButton.setIcon (updateIcon);
// 	eraseButton =
// 	    createButton (" Erase ",
// 			  "Remove everything from the local cache",
// 			  KeyEvent.VK_E,
// 			  new ActionListener() {
// 			      public void actionPerformed (ActionEvent e) {
// 				  onCacheErase();
// 			      }
// 			  });
// 	eraseButton.setIcon (eraseIcon);

	JPanel buttonPanel = createButtonPanel (new JButton[] { infoButton,
								updateButton });
// 								eraseButton });
	onUseCache (usingCache);

	JPanel cLocation = createTitledPanel ("Local cache");
 	SwingUtils.addComponent (cLocation, useCache,      0, 0, 1, 1, NONE, NWEST, 0.0, 0.0);
  	SwingUtils.addComponent (cLocation, labelCacheDir, 0, 1, 1, 1, NONE, NWEST, 0.0, 0.0, BREATH_TOP);
	SwingUtils.addComponent (cLocation, cacheDir,      0, 2, 1, 1, HORI, NWEST, 1.0, 0.0);
 	SwingUtils.addComponent (cLocation, buttonPanel,   0, 3, 1, 1, NONE, SWEST, 0.0, 0.0);

	return cLocation;
    }

    /**************************************************************************
     *
     **************************************************************************/
    public String getName() {
	return "Registry Browser";
    }

    /**************************************************************************
     *
     **************************************************************************/
    public String getDescription() {
	return
	    "A panel showing all Biomoby entities, allowing different sort orders. " +
	    "It also defines which Biomoby registry to use and how and where " +
	    "to cache Biomoby entities locally.";
    }

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

}
