package ca.ucalgary.seahawk.gui;

import ca.ucalgary.seahawk.util.HTMLUtils;
import ca.ucalgary.seahawk.util.MobyUtils;
import ca.ucalgary.seahawk.services.MobyClient;
import org.biomoby.shared.MobyPrefixResolver;
import org.biomoby.shared.*;
import org.biomoby.shared.data.*;
import org.biomoby.shared.parser.MobyTags;

import org.w3c.dom.*;
import javax.xml.parsers.*;

import java.io.*;
import java.net.*;
import java.util.*;
import java.awt.*;
import java.awt.datatransfer.*;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.text.*;

/*
 * This class allows Seahawk to accept drop events from the native
 * windowing system, so dropped files, URLs and strings, if they can be represented as MobyObjects,
 * can be loaded into a desired component. 
 *
 * This class is somewhat similar to FileAndTextTransferHandler, but enforces that the data must 
 * be convertible into a desired Moby Object data type in order to be dropped.  Components using this
 * class as their TransferHandler should implement MobyObjectReceiver for this to do something meaningful.
 */
public class MobyObjectTransferHandler extends FileAndTextTransferHandler implements ClipboardOwner{
    private MobyClient client;
    private static Map<MobyClient,MobyObjectTransferHandler> handlers;

    protected MobyObjectTransferHandler(MobyClient cl) {
	super(null);
	client = cl;
    }

    /**
     * This is how you get an instance of the handler.  Each MobyClient
     * has a singleton handler.
     */
    public static MobyObjectTransferHandler getHandler(MobyClient cl){
	if(handlers == null){
	    handlers = new HashMap<MobyClient,MobyObjectTransferHandler>();
	}
	if(!handlers.containsKey(cl)){
	    handlers.put(cl, new MobyObjectTransferHandler(cl));
	}
	//System.err.println("Returning handler " + handlers.get(cl));
	return handlers.get(cl);
    }

   @SuppressWarnings("unchecked")
    public boolean importData(JComponent c, Transferable t) {
	MobyObjectReceiver receiver = null;

	if(!(c instanceof MobyObjectReceiver)){
	    System.err.println("Cannot drop data onto Seahawk component (" + c + 
			       "), it is not a MobyObjectReceiver.  The component should not " +
			       "have MobyObjectTransferHandler registered as its TransferHandler");
            return false;
	}
	receiver = (MobyObjectReceiver) c;

	//System.err.println("Received drop event");
	Map<String, MobyPrimaryData> acceptableData = receiver.getAcceptableData();
	// Does the component even want data?
	if(acceptableData == null || acceptableData.isEmpty()){
	    return false;
	}

        if (!canImport(c, t.getTransferDataFlavors())) {
            System.err.println("Cannot drop data into Seahawk targeted type: " + t);
            return false;
        }

	// Keep track of all the object we find
	Map<String,MobyDataInstance> dataFound = null;

        // Should be done in another thread to not block UI
        try {
	    DataFlavor mobyFlavor = getMobyFlavor(t.getTransferDataFlavors());
	    if(mobyFlavor != null){
		MobyDataInstance mobyData = (MobyDataInstance) t.getTransferData(mobyFlavor);
		dataFound = new HashMap<String,MobyDataInstance>();
		dataFound.put(mobyData.getName(), mobyData);
	    }
            else if (hasFileFlavor(t.getTransferDataFlavors())) {
		dataFound = getDataFromFiles(acceptableData, 
					     (java.util.List<File>) t.getTransferData(DataFlavor.javaFileListFlavor));
            }
	    else if (hasTextFlavor(t.getTransferDataFlavors())) {
                // Make a string out of the text
		boolean PLAIN_TEXT = true;
		String text = convertToString(t, PLAIN_TEXT);

		// If the text looks like a MOBY XML payload, be nice and create
		// the proper object to load
		MobyContentInstance content = HTMLUtils.checkForMobyXML(text);
		if(content != null){
		    //System.err.println("Data appears to be moby XML...");
		    dataFound = getDataFromMobyContents(acceptableData, content);
		    return sendDataToReceiver(receiver, acceptableData, dataFound);
		}

		// Is it an address bar icon dragged from firefox?  Strip the surrounding html
		String linktext = HTMLUtils.checkForHyperlinkText(text);

                // See if it's a URL
                URL u = null;
                try{
		    u = new URL(linktext == null ? text : linktext);
                } catch(MalformedURLException murle){
		    // Not a URL, oh well, move on to string handling...
                }
                if(u != null){
		    // Does the URL itself encode any objects (as opposed to its contents)?
		    //System.err.println("Data appears to be a URL...");
                    dataFound = getDataFromURL(acceptableData, u);
		    // Does the contents of the URL?
		    mergeDataMaps(dataFound, getDataFromURLContents(acceptableData, u));
		    return sendDataToReceiver(receiver, acceptableData, dataFound);
	        }

                // Otherwise take the data as-is and see if it contains anything that can
		// be turned into a MOBY object by some MOB rule
		//System.err.println("Data appears to be text..."+text);
		dataFound = getDataFromText(acceptableData, text);
            }  //end else if(text)
	    else{
		System.err.println("Cannot drop data into Seahawk targeted type, not a Moby Java object, a file or convertible to text");
	    }
        } catch (UnsupportedFlavorException ufe) {
            System.out.println("importMobyData: unsupported data flavor: "+ufe);
        } catch (IOException ioe) {
	    ioe.printStackTrace();
            System.out.println("importMobyData: I/O exception: "+ioe);
        } catch (Exception e) {
	    e.printStackTrace();
	    System.out.println("importMobyData: General exception: "+e);
	}
	return sendDataToReceiver(receiver, acceptableData, dataFound);
    }

    public boolean sendDataToReceiver(MobyObjectReceiver receiver, 
				      Map<String,MobyPrimaryData> acceptableData,
				      Map<String,MobyDataInstance> dataFound){
	if(dataFound == null || dataFound.isEmpty()){
	    //System.err.println("Received data was blank, not sending to consumer");
	    return false;
	}
	// Unambiguously found a MOBY object or object collection mapping for the pasted data 
	if(dataFound.size() == 1 && acceptableData.size() == 1){
	    MobyDataInstance newData = dataFound.values().iterator().next();
	    MobyPrimaryData dataTemplate = acceptableData.values().iterator().next();
	    String dataName = acceptableData.keySet().iterator().next();

	    //System.err.println("Received data match 1 to 1, forwarding to " + receiver + " the data" +newData.toString());
	    if(newData instanceof MobyDataObject){
		if(dataTemplate instanceof MobyPrimaryDataSimple){
		    receiver.consumeMobyObject(dataName, newData);
		}
		// Cast single value to collection
		else{
		    MobyDataObjectSet newDataCollection = new MobyDataObjectSet("");
		    newDataCollection.add((MobyDataObject) newData);
		    receiver.consumeMobyObject(dataName, newDataCollection);
		}
	    }
	    // must be a MobyDataObjectSet?
	    else{
		if(dataTemplate instanceof MobyPrimaryDataSet){
		    receiver.consumeMobyObject(dataName, newData);
		}
		else if(((MobyDataObjectSet) newData).size() == 1){
		    receiver.consumeMobyObject(dataName, ((MobyDataObjectSet) newData).iterator().next());
		}
		// The receiver only wants one value, but we have a bunch
		else{
		    // TODO let user select from the bunch
		    System.err.println("TODO let user select one value from the bunch of data available");
		}
	    }
	    
	    return true;
	}
	//System.err.println("Received data does not match 1 to 1, forwarding not yet implemented");
	// Otherwise the user will have to choose the object to paste
	// from the list of Moby Objects created from the pasted data
	// TODO
	return false;
    }

    public Map<String, MobyDataInstance> getDataFromMobyContents(Map<String,MobyPrimaryData> acceptableData, 
								 MobyContentInstance mci){
	Map<String, MobyDataInstance> foundData = new HashMap<String,MobyDataInstance>();

	// Go through each object in each job, looking for objects of the appropriate data type
	int itemCount = 1;
	for(MobyDataJob job: mci.values()){
	    for(MobyDataInstance mdi: job.values()){
		if(mdi instanceof MobyDataObject){
		    MobyDataObject object = (MobyDataObject) mdi;
		    MobyDataType objectDataType = object.getDataType();
		    for(String fieldName: acceptableData.keySet()){
			if(objectDataType.inheritsFrom(acceptableData.get(fieldName).getDataType())){
			    foundData.put(fieldName+"#"+itemCount++, object);
			}
		    }
		    // TODO in future, maybe we should look inside composites
		    // to see if the appropriate datatype exists...
		}
	    }
	}
	return foundData;
    }

    public Map<String, MobyDataInstance> getDataFromText(Map<String, MobyPrimaryData> acceptableData, 
							 String text){
	Map<String, MobyDataInstance> foundData = new HashMap<String,MobyDataInstance>();
	
	for(String fieldName: acceptableData.keySet()){
	    int itemCount = 1;
	    MobyDataType desiredDataType = acceptableData.get(fieldName).getDataType();
	    String dataTypeName = desiredDataType.getName();
	    //System.err.println("Checking text for field " + fieldName+ ", data type " + desiredDataType.getName());

	    // trivial data creations first ...
	    if(dataTypeName.equals(MobyTags.MOBYSTRING)){
		foundData.put(fieldName+"#"+itemCount++, new MobyDataString(text));
	    }
	    else if(dataTypeName.equals(MobyTags.MOBYBOOLEAN) && 
		    text.matches("(true|True|TRUE|1|yes|Yes|YES|false|False|FALSE|0|no|No|NO)")){
		foundData.put(fieldName+"#"+itemCount++, 
			      new MobyDataBoolean(text.matches("(true|True|TRUE|1|yes|Yes|YES)")));
	    }
	    else if(dataTypeName.equals(MobyTags.MOBYINTEGER) && 
		    text.matches("\\d+")){
		foundData.put(fieldName+"#"+itemCount++, new MobyDataInt("", text));
	    }
	    else if(dataTypeName.equals(MobyTags.MOBYFLOAT) && 
		    text.matches("\\d+(?:\\.\\d*)?(?:[eE][\\-\\+]?\\d+)?")){
		foundData.put(fieldName+"#"+itemCount++, new MobyDataFloat(text));
	    }
	    else if(dataTypeName.equals(MobyTags.MOBYDATETIME) && 
		    text.matches("(?:19|20)\\d\\d-(?:0[1-9]|1[012])-([012][1-9]|3[01])")){
		foundData.put(fieldName+"#"+itemCount++, new MobyDataDateTime(text));
	    }
	    MobyDataObject[] textData = client.getMobyObjects(text, desiredDataType);
	    if(textData != null){
		//System.err.println("Found " + textData.length + " matches");
		for(MobyDataObject object: textData){
		    foundData.put(fieldName+"#"+itemCount++, object);
		}
	    }
	}
	return foundData;
    }

    public Map<String, MobyDataInstance> getDataFromURL(Map<String, MobyPrimaryData> acceptableData, 
							URL url){
	Map<String, MobyDataInstance> foundData = new HashMap<String,MobyDataInstance>();

	// Does the URL itself represent any objects
	for(String fieldName: acceptableData.keySet()){
	    int itemCount = 1;
	    MobyDataType desiredDataType = acceptableData.get(fieldName).getDataType();
	    MobyDataObject[] urlData = client.getMobyObjects(url, desiredDataType);
	    if(urlData != null){
		for(MobyDataObject object: urlData){
		    foundData.put(fieldName+"#"+itemCount++, object);
		}
	    }
	}

	return foundData;
    }

    public Map<String, MobyDataInstance> getDataFromURLContents(Map<String, MobyPrimaryData> acceptableData, 
								URL u) throws Exception{
	Map<String, MobyDataInstance> foundData = new HashMap<String,MobyDataInstance>();
	if(u == null){
	    return foundData;
	}

	ByteArrayOutputStream byteBuffer = new ByteArrayOutputStream();
	InputStream inputStream = u.openStream();
	byte[] dataChunk = new byte[4096];
	for(int bytesRead = inputStream.read(dataChunk); bytesRead != -1; bytesRead = inputStream.read(dataChunk)){
	    byteBuffer.write(dataChunk, 0, bytesRead);
	}
	for(String fieldName: acceptableData.keySet()){
	    int itemCount = 1;
	    MobyDataType desiredDataType = acceptableData.get(fieldName).getDataType();
	    MobyDataObject[] fileData = client.getMobyObjects(byteBuffer.toByteArray(), desiredDataType);
	    if(fileData != null){
		for(MobyDataObject object: fileData){
		    foundData.put(fieldName+"#"+itemCount++, object);
		}
	    }
	}
	
	return foundData;
    }

    public Map<String, MobyDataInstance> getDataFromFiles(Map<String, MobyPrimaryData> acceptableData, 
							  Iterable<File> files) throws Exception{
	Map<String, MobyDataInstance> foundData = new HashMap<String,MobyDataInstance>();

	// Load any dragged file as a URL
	for (File file: files) {
	    // Windows Web shortcut?
	    URL u = HTMLUtils.checkForURLShortcut(file);

	    if(u != null){
		foundData = getDataFromURL(acceptableData, u);
	    }
	    // Any other type of file is opened and its contents checked for Moby Objects
	    else{
		try{u = file.toURI().toURL();}
                catch(Exception e){e.printStackTrace();}
	    }

	    mergeDataMaps(foundData, getDataFromURLContents(acceptableData, u));
	}
	return foundData;
    }

    public void mergeDataMaps(Map<String, MobyDataInstance> keeper, Map<String, MobyDataInstance> newData){
	if(newData == null){
	    return;
	}
	for(Map.Entry<String, MobyDataInstance> entry: newData.entrySet()){
	    String key = entry.getKey();
	    if(keeper.containsKey(key)){
		// TODO: what if the name is duplicate?		    
	    }
	    keeper.put(key, entry.getValue());
	}
    }

    /**
     * Drag and copy functiunality.
     */
    public void exportToClipboard(JComponent comp,
				  Clipboard clip,
				  int action) throws IllegalStateException{
	Transferable createdData = null;
	try{
	    if(!(comp instanceof MobyContentProducer)){
		throw new IllegalStateException("Component calling exportToClipboard " +
						"was not a MobyObjectProducer as expected, export aborted");
	    }
	    createdData = MobyUtils.createTransferable(((MobyContentProducer) comp).exportMobyContent());
	    clip.setContents(createdData, this);
	} catch(IllegalStateException e){
	    // To be consistent with the normal transfer handler, call exportDone
	    // even if we failed
	    exportDone(comp, createdData, TransferHandler.NONE);
	    throw e;
	}
	exportDone(comp, createdData, action);
    }
    
    /**
     * To satisfy the ClipboardOwner interface.
     */
    public void lostOwnership(Clipboard clipboard, Transferable contents){
	
    }

    protected DataFlavor getMobyFlavor(DataFlavor[] flavors) {
        for (DataFlavor flavor: flavors) {
            if (flavor.getRepresentationClass().getName().equals(MobyDataInstance.class.getName())) {
                return flavor;
            }
        }
        return null;
    }
}
