// ResultsPanel.java
//
// Created: February 2006
//
// 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.MobyException;
import org.biomoby.shared.parser.MobyPackage;
import org.biomoby.shared.parser.MobyJob;
import org.biomoby.shared.parser.MobyDataElement;
import org.biomoby.shared.parser.MobySimple;
import org.biomoby.shared.parser.MobyCollection;
import org.biomoby.shared.datatypes.MobyObject;
import org.biomoby.service.dashboard.data.DataContainer;
import org.biomoby.service.dashboard.renderers.RendererRegistry;
import org.biomoby.service.dashboard.renderers.Renderer;

import org.tulsoft.shared.UUtils;
import org.tulsoft.tools.gui.SwingUtils;

import javax.swing.JPanel;
import javax.swing.JLabel;
import javax.swing.JComboBox;
import javax.swing.Icon;
import javax.swing.Box;
import javax.swing.JButton;
import javax.swing.JFileChooser;
import javax.swing.JComponent;
import javax.swing.JTabbedPane;
import javax.swing.DefaultComboBoxModel;

import java.awt.GridBagLayout;
import java.awt.Component;
import java.awt.Color;
import java.awt.Insets;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.event.ChangeListener;
import javax.swing.event.ChangeEvent;

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


/**
 * A panel displaying service result data, using available
 * renderers. This is not, however, used as a regular Dashboard panel
 * - but it appears as a sub-panelof a SimpleClientPanel, and can be
 * detached from it and become a stanalone frame. <p>
 *
 * @author <A HREF="mailto:martin.senger@gmail.com">Martin Senger</A>
 * @version $Id: ResultsPanel.java,v 1.4 2006/02/20 05:51:10 senger Exp $
 */

public class ResultsPanel
    extends AbstractPanel
    implements ChangeListener {

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

    protected static final String SAVE_ERROR =
	"Sorry, an error happened when saving...\n\n";

    protected static final String VIEWER_ERROR =
	"Sorry, an error happened when displaying a result...\n\n";

    protected static final String VIEWER2_ERROR =
	"Sorry, an error happened when preparing a result.\n\n" +

	"If it is because of missing generated Java classes\n" +
	"(see the error text below) then you may see less\n" +
	"viewers than it is available for this particular\n" +
	"result. Please consider to follow hints (if any)\n" +
	"in the message below to rectify the problem.\n\n" +

	"Please note that this error will be reported only\n" +
	"the first time when is encountered.\n\n";

    protected static final String NOT_SAVED =
	"Sorry, the contents was not saved. It is possible\n" +
	"that the current viewer does not support saving.\n\n";

    protected static final Insets B_TOP = new Insets (2,0,0,0);

    // each item in this list corresponds to one tab
    protected ArrayList results = new ArrayList();
    protected RendererRegistry rendRegistry;

    // components
    JButton detachButton;
    JButton cleanButton, saveButton;
    JFileChooser saveChooser;
    JComboBox viewers;
    JTabbedPane resultsPane;
    boolean alreadyReported = false;

    // shared icons
    static Icon cleanIcon, cleanIconDis;
    static Icon saveIcon, saveIconDis;
    static Icon detachIcon, detachIconDis;

    /**************************************************************************
     * Make chnages needed for a standalone result panel.
     **************************************************************************/
    public void adjustForDetachement() {
	if (pComponent != null) {
	    pComponent.remove (detachButton);
	    pComponent.remove (cleanButton);
	}
    }

    /**************************************************************************
     * This creates an empty panel, without any data displayed. To
     * fill it with data, use {@link #updateComponent}. <p>
     **************************************************************************/
    public JComponent getComponent (PropertyChannel aPropertyChannel) {
 	setPropertyChannel (aPropertyChannel);

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

	pComponent.setBorder (createFatBorder
			      ("Service Results",
			       GraphColours.getColour ("cadetblue", Color.blue)));

	resultsPane = new JTabbedPane();
	resultsPane.addChangeListener (this);

	viewers = new JComboBox (new DefaultComboBoxModel());
	viewers.setEnabled (false);
        viewers.setToolTipText ("Available viewers");
	viewers.addActionListener (new ActionListener() {
		public void actionPerformed (ActionEvent e) {
		    int index = resultsPane.getSelectedIndex();
		    if (index > -1) {
			ResultContainer res = (ResultContainer)results.get (index);
			JComboBox cb = (JComboBox)e.getSource();
			DelegateRenderer dr = (DelegateRenderer)cb.getSelectedItem();
			if (dr != null) {
			    JComponent comp = dr.getComponent();
			    if (comp != null) {
				resultsPane.setComponentAt (index, comp);
				resultsPane.setIconAt (index, dr.getIcon());

				// remember as 'last shown'
				int current = cb.getSelectedIndex();
				if (current > -1)
				    res.setIndexOfLastShown (current);
			    }
			}
		    }
		}
	    });

	cleanButton = createButton
	    ("",
	     "Discard all results",
	     -1,
	     new ActionListener() {
		 public void actionPerformed (ActionEvent e) {
		     if (results.size() > 0 &&
			 AbstractPanel.confirm (ResultsPanel.this.getParent(),
						"Discard all results?")) {
			 removeResults();
		     }
		 }
	     });
	cleanButton.setIcon (cleanIcon);
	cleanButton.setDisabledIcon (cleanIconDis);
	cleanButton.setEnabled (false);
	SwingUtils.compact (cleanButton);

	saveChooser = new JFileChooser();
	String initValue = getPrefValue (DP_RESULT_FILE,
					 System.getProperty ("user.dir"));
	saveChooser.setDialogTitle ("Save result");
	if (UUtils.notEmpty (initValue)) {
	    File file = new File (initValue);
	    saveChooser.setSelectedFile (file);
	}

	saveButton = createButton
	    ("",
	     "Save displayed result to a file",
	     -1,
	     new ActionListener() {
		 public void actionPerformed (ActionEvent e) {
		     onSave();
		 }
	     });
	saveButton.setIcon (saveIcon);
	saveButton.setDisabledIcon (saveIconDis);
	saveButton.setEnabled (false);
	SwingUtils.compact (saveButton);

	detachButton = createButton
	    ("",
	     "Detach this result panel from the Dashboard",
	     -1,
	     new ActionListener() {
		 public void actionPerformed (ActionEvent e) {
		     propertyChannel.fire (DP_DETACH_VIEW, "");
		 }
	     });
	detachButton.setIcon (detachIcon);
	detachButton.setDisabledIcon (detachIconDis);
	detachButton.setEnabled (false);
	SwingUtils.compact (detachButton);

	// put it together
	Component glue = Box.createHorizontalGlue();
   	SwingUtils.addComponent (pComponent, resultsPane,  0, 0, 5, 1, BOTH, NWEST,  1.0, 1.0);
 	SwingUtils.addComponent (pComponent, cleanButton,  0, 1, 1, 1, NONE, NWEST,  0.0, 0.0, B_TOP);
 	SwingUtils.addComponent (pComponent, saveButton,   1, 1, 1, 1, NONE, NWEST,  0.0, 0.0, B_TOP);
 	SwingUtils.addComponent (pComponent, detachButton, 2, 1, 1, 1, NONE, NWEST,  0.0, 0.0, B_TOP);
 	SwingUtils.addComponent (pComponent, glue,         3, 1, 1, 1, HORI, NWEST,  1.0, 0.0, B_TOP);
 	SwingUtils.addComponent (pComponent, viewers,      4, 1, 1, 1, NONE, NEAST,  0.0, 0.0, B_TOP);

	return pComponent;
    }

    /*********************************************************************
     * Load all shared icons.
     ********************************************************************/
    protected void loadIcons() {
	super.loadIcons();

	if (cleanIcon == null) cleanIcon = loadIcon ("images/smallClear.gif");
	if (cleanIconDis == null) cleanIconDis = loadIcon ("images/smallClear_dis.gif");

	if (saveIcon == null) saveIcon = loadIcon ("images/smallSave.gif");
	if (saveIconDis == null) saveIconDis = loadIcon ("images/smallSave_dis.gif");

	if (detachIcon == null) detachIcon = loadIcon ("images/smallDetach.gif");
	if (detachIconDis == null) detachIconDis = loadIcon ("images/smallDetach_dis.gif");
    }

    /*********************************************************************
     * Show...
     ********************************************************************/
    protected String showSaveDialog() {
	if (saveChooser.showSaveDialog (this) != JFileChooser.APPROVE_OPTION)
	    return null;
	String fileName = saveChooser.getSelectedFile().getAbsolutePath();
	setPrefValue (DP_RESULT_FILE, fileName);
	return fileName;
    }

    /*********************************************************************
     * Save contents...
     ********************************************************************/
    protected void onSave() {

	final String fileName = showSaveDialog();
	if (UUtils.isEmpty (fileName ))
	    return;

	final SwingWorker worker = new SwingWorker() {
		MobyException exception = null;
		public Object construct() {
		    try {
			DelegateRenderer dr =
			    (DelegateRenderer)viewers.getSelectedItem();
			if (dr != null)
			    if (! dr.save2File (new File (fileName)) )
				error (NOT_SAVED);
		    } catch (MobyException e) {
			exception = e;
		    }
		    return null;  // not used here
		}
		    
		// runs on the event-dispatching thread
		public void finished() {
		    if (exception != null)
			error (SAVE_ERROR, exception);
		}
	    };
	worker.start(); 
    }

    /*********************************************************************
     * Remove contents...
     ********************************************************************/
    protected void removeResults() {
	resultsPane.removeAll();
	results = new ArrayList();
	viewers.removeAllItems();
	viewers.setEnabled (false);
	cleanButton.setEnabled (false);
	saveButton.setEnabled (false);
	detachButton.setEnabled (false);
    }

    /*********************************************************************
     * Select contents...
     ********************************************************************/
    protected void selectResult (int index) {
	updateViewers (index);
	viewers.setEnabled (true);
	cleanButton.setEnabled (true);
	saveButton.setEnabled (true);
	detachButton.setEnabled (true);
    }

    /**************************************************************************
     *
     **************************************************************************/
    public void updateComponent (DataContainer data) {

	// make sure that the main component of this panel was initialized
	if (resultsPane == null) {
	    log.error ("updateComponent() called before getComponent()");
	    return;
	}
	removeResults();

	// load renderers (done only the first time it is called)
	rendRegistry = RendererRegistry.instance();

	// find viewers for the whole result (for all service outputs
	// together)
	ResultContainer forAll = new ResultContainer ("All");
	results.add (forAll);
	findAndAddRenderers (Renderer.CLASS_NAME,
			     data.getData().getClass().getName(),
			     forAll, data, rendRegistry);
	if ( data.getData() instanceof String &&
	     ((String)data.getData()).startsWith ("<?xml") ) {
	    findAndAddRenderers (Renderer.MIME_TYPE, "text/xml",
				 forAll, data, rendRegistry);
	}

	// viewers for individual service outputs (for that we need to
	// create a Moby Java objects - which may fail if the
	// generated clases are not present - but we report it only
	// the first time)
	MobyPackage moby = null;
	try {
	    moby = MobyPackage.createFromXML (data.getData());

	} catch (MobyException e) {
	    if (! alreadyReported) {
		alreadyReported = true;
		error (VIEWER2_ERROR, e);
	    }
	}
	if (moby != null) {

	    // still for the whole returned package
	    DataContainer mobyData = new DataContainer (moby, propertyChannel);
	    findAndAddRenderers (Renderer.MOBY_TYPE, "",
				 forAll, mobyData, rendRegistry);
	    MobyJob[] jobs = moby.getJobs();
	    for (int i = 0; i < jobs.length; i++) {
		MobyJob job = jobs[i];
		MobyDataElement[] outputs = job.getDataElements();
		for (int j = 0; j < outputs.length; j++) {
		    MobyDataElement output = outputs[j];
		    String resultName = output.getName();
		    if (jobs.length > 1)
			resultName = resultName + " (" + job.getId() + ")";
		    ResultContainer rescon = new ResultContainer (resultName);
		    processOneOutput (rescon, output, rendRegistry);
		    results.add (rescon);
		}
	    }
	}

	// create viewers components for every result (put them in the
	// tabbed pane)
	boolean somethingToDisplay = false;
        for (Iterator iter = results.iterator(); iter.hasNext();) {
	    ResultContainer res = (ResultContainer) iter.next();
	    JComponent comp = res.getDefaultComponent();
	    if (comp != null) {
		resultsPane.addTab (res.getResultName(),
				    res.getDefaultIcon(),
				    comp);
		somethingToDisplay = true;
	    }
	}

	// switch on the first tab
	if (somethingToDisplay) {
	    selectResult (0);
	} else {
	    resultsPane.addTab ("Empty", new JLabel ("No results, or no viewers..."));
	}

    }

    //
    protected boolean findAndAddRenderers (String criterion,
					   String value,
					   ResultContainer resultContainer,
					   DataContainer data,
					   RendererRegistry registry) {
	boolean somethingAdded = false;
	List rs = registry.getRenderers (criterion, value);
        for (Iterator i = rs.iterator(); i.hasNext();) {
	    resultContainer.addRenderer ((Renderer) i.next(), data);
	    somethingAdded = true;
	}
	return somethingAdded;
    }


    //
    protected void processOneOutput (ResultContainer rescon,
				     MobyDataElement output,
				     RendererRegistry registry) {
	if (output instanceof MobySimple) {
	    MobyObject mObj = ((MobySimple)output).getData();
	    if (mObj != null) {
		DataContainer mobyResult = new DataContainer (mObj, propertyChannel);
		findAndAddRenderers (Renderer.MOBY_TYPE,
				     mObj.getMobyTypeName(),
				     rescon, mobyResult, rendRegistry);
	    }

	} else if (output instanceof MobyCollection) {
	    MobySimple[] elems = ( (MobyCollection)output ).getData();
	    if (elems.length > 0) {
		ArrayList a = new ArrayList();
		for (int k = 0; k < elems.length; k++) {
		    MobyObject mo = elems[k].getData();
		    if (mo != null)
			a.add (mo);
		}
		if (a.size() > 0) {
 		    MobyObject[] collection = new MobyObject [a.size()];
		    a.toArray (collection);
		    MobyObject mObj0 = collection[0];
		    DataContainer mobyColl = new DataContainer (collection, propertyChannel);
		    findAndAddRenderers (Renderer.MOBY_TYPE,
					 mObj0.getMobyTypeName(),
					 rescon, mobyColl, rendRegistry);
		}
	    }
	}
    }

    /**************************************************************************
     * Called when a new tab was selected. We need to change the
     * viewers combo box to get new viewers names.
     **************************************************************************/
    public void stateChanged (ChangeEvent e) {
	int index = resultsPane.getSelectedIndex();
	if (index > -1)   // opposite can happen when all tabs are being removed
	    updateViewers (index);
    }


    /**************************************************************************
     * 'index' is an index of currently selected tab
     **************************************************************************/
    protected void updateViewers (int index) {
	ResultContainer res = (ResultContainer)results.get (index);

	ActionListener[] als = viewers.getActionListeners();
	for (int l = 0; l < als.length; l++)
	    viewers.removeActionListener (als[l]);

	viewers.removeAllItems();
	int i = 0;
	List rs = res.getRenderers();
        for (Iterator iter = rs.iterator(); iter.hasNext();) {
	    Renderer r = (Renderer) iter.next();
	    viewers.addItem (new DelegateRenderer (r, res.getDataContainer (i)));
	    i++;
	}

	for (int l = 0; l < als.length; l++)
	    viewers.addActionListener (als[l]);

 	viewers.setSelectedIndex (res.getIndexOfLastShown());
    }

    private class DelegateRenderer {
	Renderer delegate;
	DataContainer data;
	public DelegateRenderer (Renderer realRenderer,
				 DataContainer data) {
	    delegate = realRenderer;
	    this.data = data;
	}
	public JComponent getComponent() {
	    try {
		return delegate.getComponent (data);
	    } catch (MobyException e) {
		error (VIEWER_ERROR, e);
	    }
	    return null;
	}
	public Icon getIcon() {
	    return delegate.getIcon();
	}
	public boolean save2File (File file)
	    throws MobyException {
	    return delegate.save2File (data, file);
	}

	// this is why we have this class, in the first place
	public String toString() {
	    return delegate.getName();
	}
    }

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

    /*********************************************************************
     *
     * A richer DataContainer that already knows about its viewers.
     *
     ********************************************************************/

    protected class ResultContainer {

	// these two lists must be in sync
	ArrayList data      = new ArrayList();  // type: DataContainer
	ArrayList renderers = new ArrayList();  // type: Renderer

	// the name that will appear in the 'tab' header for this
	// result container
	String resultName;

	// index of last shown renderer
	int lastShown = 0;

	// constructor
	public ResultContainer (String resultName) {
	    this.resultName = resultName;
	}

	public String getResultName() {
	    return resultName;
	}

	// remember a 'renderer' that can render 'data' (but only if
	// we do not have it already)
	public synchronized void addRenderer (Renderer r,
					      DataContainer container) {
	    String rendererName = r.getName();
	    for (Iterator iter = renderers.iterator(); iter.hasNext();) {
		if ( rendererName.equals (((Renderer) iter.next()).getName()) )
		    return;
	    }
	    renderers.add (r);
	    data.add (container);
	}

	public List getRenderers() {
	    return renderers;
	}

	public DataContainer getDataContainer (int index) {
	    return (DataContainer)data.get (index);
	}

	public JComponent getDefaultComponent() {
	    if (renderers.size() > 0) {
		try {
		    return ((Renderer)renderers.get (0))
			.getComponent ( (DataContainer)data.get (0) );
		} catch (MobyException e) {
		    error (VIEWER_ERROR, e);
		}
	    }
	    return null;
	}

	public Icon getDefaultIcon() {
	    if (renderers.size() > 0)
		return ((Renderer)renderers.get (0)).getIcon();
	    return null;
	}

 	public int getIndexOfLastShown() {
	    return lastShown;
	}
 	public void setIndexOfLastShown (int lastShown) {
	    this.lastShown = lastShown;
	}

    }

}
