
package ca.ucalgary.seahawk.gui;

import ca.ucalgary.seahawk.services.MobyClient;
import ca.ucalgary.seahawk.util.*;

import org.biomoby.client.MobyRequestEventHandler;
import org.biomoby.shared.MobyDataType;
import org.biomoby.shared.MobyNamespace;
import org.biomoby.shared.data.*;
import org.biomoby.shared.parser.MobyTags;

import javax.swing.*;
import javax.xml.transform.Transformer;
import javax.xml.parsers.DocumentBuilder;

import java.awt.Color;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.File;
import java.io.FileOutputStream;
import java.net.URL;
import java.util.*;
import java.util.logging.*;
import java.util.regex.Pattern;

/**
 * The clipboard is a special tab that holds a query with a MOBY Collection
 * that the user can add and subtract MobyDataInstances from.
 */
public class MobyContentClipboard extends MobyContentPane implements DataImportChoiceListener{
    public final static String CLEAR_CLIPBOARD_OPTION = "Clear Clipboard";
    public final static String CLIPBOARD_FILE_NAME = "SeaHawkClipboard";
    public final static String CLIPBOARD_COLLECTION_NAME = "Clipboard Collection";
    public final static String CLIPBOARD_QUERY_NAME = "Clipboard";
    public final static String CLIPBOARD_TAB_NAME = "Clipboard";
    public final static Color CLIPBOARD_TAB_COLOR = Color.blue;

    public static final String CLIPBOARD_TAB_ICON_RESOURCE = "ca/ucalgary/seahawk/resources/images/clipboard.gif";

    private static ImageIcon clipboardIcon;

    private MobyContentInstance content;
    private File clipboardFile;
    private MobyDataJob query;  // lump all data into one query
    private MobyDataObjectSet collection;  // lump all data into one collection
    private MobyDataType previousImportDataType = null;
    private MobyNamespace previousImportNS = null;

    private Pattern commentRegex;
    private Pattern scriptRegex;
    private Pattern styleRegex;
    private Pattern tagRegex;

    // Clipboard editing variables
    private JMenuItem deleteDataPopupItem;
    private MobyDataInstance itemToDelete;
    private static Logger logger = Logger.getLogger(MobyContentClipboard.class.getName());

    public MobyContentClipboard(MobyContentGUI cGUI, MobyServicesGUI sGUI, JTabbedPane parentComponent, 
				DataFlowRecorder recorder, JLabel statusBar){
	super(cGUI, sGUI, parentComponent, recorder, statusBar);

	ClassLoader cl = getClass().getClassLoader();
	if(cl == null){
	    cl = ClassLoader.getSystemClassLoader();
	}
    	if(clipboardIcon == null){
	    URL u = cl.getResource(CLIPBOARD_TAB_ICON_RESOURCE);
	    if(u == null){
		logger.log(Level.WARNING, "Could not find icon resource " + CLIPBOARD_TAB_ICON_RESOURCE);
	    }
	    else{
		clipboardIcon = new ImageIcon(u);
	    }
	}

	query = new MobyDataJob();
	content = new MobyContentInstance();
	content.put(CLIPBOARD_QUERY_NAME, query);
	collection = new MobyDataObjectSet(CLIPBOARD_COLLECTION_NAME);

	deleteDataPopupItem = new JMenuItem("Delete from Clipboard");
	deleteDataPopupItem.addActionListener(this);

	// Delete temp file when program exits.
	try{
	    clipboardFile = File.createTempFile(CLIPBOARD_FILE_NAME, ".xml");
	}
	catch(Exception e){
	    logger.log(Level.SEVERE, "Clipboard failed to initialize, cannot create temp file", e);
	    return;
	}
	clipboardFile.deleteOnExit();

	// The dfeault is to open a new tab when contents is transfered (dropped/pasted) onto the tab,
	// we want to override that behaviour to a collation for the clipboard
	setTransferHandler(new FileAndTextTransferHandler(cGUI, false)); // false = open data in current tab
	editorPane.setTransferHandler(getTransferHandler());

	// Setup regexes used for stripping HTML docs of their markup (for data pasting ops)
	commentRegex = Pattern.compile("<!--.*?-->", Pattern.MULTILINE | Pattern.DOTALL);
	scriptRegex = Pattern.compile("<script.*?</script>",  Pattern.MULTILINE | Pattern.DOTALL | Pattern.CASE_INSENSITIVE);
	styleRegex = Pattern.compile("<style.*?</style>",  Pattern.MULTILINE | Pattern.DOTALL | Pattern.CASE_INSENSITIVE);
	tagRegex = Pattern.compile("<.*?>", Pattern.MULTILINE | Pattern.DOTALL);

	sGUI.setClipboard(this);	
    }

    // By overriding this with a blank method body, we avoid adding a close icon to the tab
    public void stateChanged(javax.swing.event.ChangeEvent ce){
    }

    /** Sets up temporary files, icons, etc. */
    public void init(){
	int index = tabbedPane.indexOfComponent(this);
	tabbedPane.setIconAt(index, clipboardIcon);
	tabbedPane.setForegroundAt(index, CLIPBOARD_TAB_COLOR);	
	tabbedPane.setTitleAt(index, CLIPBOARD_TAB_NAME);
    }

    /** history is disabled for the clipboard */ 
    public boolean canGoBack(){
	return false;
    }
    
    /** history is disabled for the clipboard */ 
    public boolean canGoForward(){
	return false;
    }

    protected void addExtraMobyOptions(JPopupMenu popup, MobyDataInstance mdi, URL srcURL){
	super.addExtraMobyOptions(popup, mdi, srcURL);

	// Only process if the passed in data is one of the top level collection elements
	// (i.e. don't delete subparts of object)
	if(!(mdi instanceof MobyDataObject) || !collection.contains(mdi)){
	    mdi.setXmlMode(MobyDataInstance.SERVICE_XML_MODE);
	    logger.log(Level.WARNING, "Skipping " + mdi.toXML());
	    mdi.setXmlMode(MobyDataInstance.CENTRAL_XML_MODE);
	    return;
	}

	itemToDelete = mdi;
	popup.add(deleteDataPopupItem);
	popup.setVisible(true);
    }
  
    /**
     * @return true if the object was in the collection and was removed
     */ 
    public boolean removeCollectionData(MobyDataInstance itemToDelete){
	return collection.remove(itemToDelete);
    }

    public void actionPerformed(ActionEvent e){
	super.actionPerformed(e);
	if(e.getSource() == deleteDataPopupItem){
	    if(itemToDelete != null){
		collection.remove(itemToDelete);
		updateDisplay();
	    }
	}
	else{
	    logger.log(Level.WARNING, "Clipboard: ignoring unrecognized source of event ", e);
	}
    }

    /**
     * Adds a query to the clipboard with the since MobyDataInstance attached to it.
     *
     * @param title the name to show for the object on the clipboard, will replace any existing query with same title
     * @param mdi the data object to add to the clipboard
     */
    public void addData(String title, MobyDataInstance mdi){
	query.put(title, mdi);
	updateDisplay();
    }

    /**
     * The main way to add data to the clipboard, data gets appended to the query with the name given in CLIPBOARD_COLLECTION_NAME
     *
     * @param mdi the data to be added to the main clipboard collection
     */
    public void addCollectionData(MobyDataInstance mdi){
	addCollectionData(mdi, true);
    }

    public void addCollectionData(MobyDataInstance mdi, boolean updateDisplay){	
	if(mdi == null){
	    logger.log(Level.WARNING, "Cannot add null object to the clipboard.");
	    return;
	}
	if(!query.containsKey(CLIPBOARD_COLLECTION_NAME)){
	    query.put(CLIPBOARD_COLLECTION_NAME, collection);
	}
	if(mdi instanceof MobyDataObjectSet){
	    // Flatten out the set, as a MOBY set cannot contain other sets
	    collection.addAll((MobyDataObjectSet) mdi);
	}
	else if (mdi instanceof MobyDataObject){
	    collection.add((MobyDataObject) mdi);
	}
	else{
	    logger.log(Level.WARNING, "Cannot add object of class " + mdi.getClass() +
			              " to the clipboard, only MobyDataObject and MobyDataObjectSet" +
			              " are supported");
	    return;
	}

	if(updateDisplay){
	    updateDisplay();
	}
    }

    /**
     * Objects in the content instance will be flattened into a list
     * and appended to the current contents of the collection.
     */
    public void addCollectionData(MobyContentInstance mci){
	for(MobyDataJob job: mci.values()){
	    for(MobyDataInstance data: job.getPrimaryData()){
		addCollectionData(data, false); //false == don't update display 
	    }
	}
	updateDisplay();
    }

    /**
     * Attempts to add data from the given URL as MOBY objects (converting if necessary).
     * The conversion is "smart", and so if you have 2 DNASequences on the clipboard
     * already, it will try to extract DNASequences from the URL data first, then other
     * types.
     */
    public void addCollectionData(URL u){
	// See if it's a WSDL file.  If so, launch the service wrapper 
	// functionality instead of doing the collection stuff
	if(u.toString().endsWith(".wsdl")){
	    contentGUI.loadPaneFromURL(u, true); //true == new tab
	    return;  // don't load the file in the pane
	}

	try{
	    MobyContentInstance mobyContents = 
			MobyUtils.convertURLtoMobyBinaryData(servicesGUI.getMobyClient(), u);
	    if(mobyContents != null){
		addCollectionData(mobyContents);
		return;
	    }
	} catch(Exception e){
	    logger.log(Level.WARNING, "Could not transform binary file ("+u+")", e);
	}

	// Not binary, see if it's text that's convertible to MOBY data
	MobyClient client = servicesGUI.getMobyClient();
	if(client == null){
	    logger.log(Level.WARNING, "Could not get MOBY client from MOBY services GUI, " +
			              "cannot transform incoming clipboard data to MOBY objects");
	}
	String urlContents = null;
	try{
	    urlContents = HTMLUtils.getURLContents(u);
	} catch(Exception e){
	    logger.log(Level.WARNING, "Could not read contents of the URL to import to the clipboard ("+u+")", e);
	    return;
	}
	if(urlContents.indexOf("<html") != -1 || urlContents.indexOf("<HTML") != -1){
	    urlContents = tagRegex.matcher(commentRegex.matcher(styleRegex.matcher(scriptRegex.matcher(urlContents).replaceAll("")).replaceAll("")).replaceAll("")).replaceAll("");
	}
	logger.log(Level.FINE, urlContents);

	MobyDataType targetDataType = null;
	if(collection != null){
	    targetDataType = collection.getDataType();
	}
	MobyDataObject[] mobyDataFound = client.getMobyObjects(urlContents, targetDataType);
	// If only one data available, use it
	if(mobyDataFound.length == 1){
	    addCollectionData(mobyDataFound[0]);
	    return;
	}
	// If no data available, try a more general object finding query
	else if(mobyDataFound.length == 0){
	    // If the previous transformation attempt was for the base Object, there's 
	    // nothing more to search for, just treat the data as a string.
	    if(targetDataType == null || MobyTags.MOBYOBJECT.equals(targetDataType.getName())){
		addCollectionData(new MobyDataString(urlContents));
		return;
	    }
	    mobyDataFound = client.getMobyObjects(urlContents);
	    if(mobyDataFound.length == 1){
		addCollectionData(mobyDataFound[0]);
		return;
	    }
	    else if(mobyDataFound.length == 0){
		addCollectionData(new MobyDataString(urlContents));
		return;
	    }
	}

	// Otherwise we need the user to choose
	new DataImportChoiceDialog(contentGUI, mobyDataFound, previousImportDataType, previousImportNS, this);
    }

    // Callback once user has selected the data to import from the DataImportChoiceDialog
    public void importConfirmed(DataImportChoiceDialog dialog, MobyDataObject[] selectedData){	
	if(selectedData == null){
	    logger.log(Level.WARNING, "Got null for import selection, abandoning import");
	    return;
	}
	for(int i = 0; i < selectedData.length; i++){
	    addCollectionData(selectedData[i], i == selectedData.length-1); // only update display on last import
	    previousImportDataType = selectedData[i].getDataType();
	    if(selectedData[i].getPrimaryNamespace() != null){
		previousImportNS = selectedData[i].getPrimaryNamespace();
	    }
	}
    }

    public void importCanceled(DataImportChoiceDialog dialog){}

    public boolean hasXMLSource(){
	return true;
    }

    public String getXMLSource() throws java.io.IOException{
	return HTMLUtils.getInputStreamContents(clipboardFile.toURI().toURL().openStream());	    
    }

    /**
     * Overrides the default implementation in parent class because the clipboard
     * does not have a standard history.
     */
    public URL getCurrentURL(){
	try{
	    return clipboardFile.toURI().toURL();
	}
	catch(java.net.MalformedURLException murle){
	    logger.log(Level.WARNING, "Could not get clipboard file URL from "+clipboardFile, murle);
	    return null;
	}
    }

    public void updateDisplay(){
	// Write to temp file
	try{
	    FileOutputStream os = new FileOutputStream(clipboardFile);
	    MobyDataUtils.toXMLDocument(os, content);
	    os.close();
	    gotoURL(clipboardFile.toURI().toURL(), false);
	}
	catch(Exception e){
	    logger.log(Level.SEVERE, "Clipboard failed to update the display, cannot write or load temp file", e);
	    return;
	}
	tabbedPane.setTitleAt(tabbedPane.indexOfComponent(this), CLIPBOARD_TAB_NAME + 
			      ": " + getResponseType(content));
	contentGUI.setVisible(true);
    }

    /**
     * Get the Java representation of the data on the clipboard (as a MOBY data envelope), including 
     * any possible non-standard contents.  The standard way to access the clipboard is getCollection().
     * Note that changing the returned object changes the clipboard, but this is strongly discouraged
     * because the display will not reflect the changes until updateDisplay() is called.
     */
    public MobyContentInstance getContents(){
	return content;
    }

    /**
     * Get the Java representation of the data in the main clipboard collection.
     * Note that changing the returned object changes the clipboard, but this is strongly discouraged
     * because the display will not reflect the changes until updateDisplay() is called.
     */
    public MobyDataObjectSet getCollection(){
	return collection;
    }

    /**
     * If a service is requested from a link on this pane, the object returned here
     * will get the callback.  Normally, the reference is to "this", as new responses should
     * load in the same pane as the current data.  But this method returns null, menaing that 
     * MobyServiceGUI's default handler should be used.
     *
     * @return null
     */
    protected MobyRequestEventHandler getDefaultHandler(){
	return null;
    }

    public void clearData(){
	query.clear();
	collection.clear();
	updateDisplay();
    }
}
