/*******************************************************************************
 * Copyright (c) 2005 IBM Corporation and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/
package org.eclipse.bpel.ui.util;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.eclipse.bpel.common.extension.model.ExtensionMap;
import org.eclipse.bpel.model.Activity;
import org.eclipse.bpel.model.CorrelationSet;
import org.eclipse.bpel.model.PartnerLink;
import org.eclipse.bpel.model.Source;
import org.eclipse.bpel.model.Sources;
import org.eclipse.bpel.model.Target;
import org.eclipse.bpel.model.Targets;
import org.eclipse.bpel.model.Variable;
import org.eclipse.bpel.model.resource.BPELResource;
import org.eclipse.bpel.model.resource.BPELWriter;
import org.eclipse.bpel.model.util.BPELConstants;
import org.eclipse.bpel.ui.adapters.IContainer;
import org.eclipse.emf.ecore.EObject;
import org.eclipse.emf.ecore.resource.Resource;
import org.eclipse.swt.dnd.Clipboard;
import org.eclipse.swt.dnd.TextTransfer;
import org.eclipse.swt.dnd.Transfer;
import org.eclipse.swt.widgets.Display;

/**
 * The transfer buffer is the de-facto clipboard that us being used by the BPEL editor.
 * 
 * Each BPEL editor has its own transfer buffer. The objects in the transfer buffer
 * are just clones of the selected EMF objects. A little post processing is done to make
 * sure that in the case of a multiple selection only the top most objects of any tree
 * are in the selection (so if you select "empty" and a "sequence" in which the "empty"
 * resides then only "sequence" is copied to the transfer buffer.
 * 
 * In addition to coping the objects to the transfer buffer a serialized version of the
 * them is dumped into the system clipboard as BPEL XML. This allows for cross-editor copy/paste
 * that works reasonably well.
 * 
 * It also allows for valid BPEL XML to be pasted into the editor from other (textual) representations.
 * 
 *  
 * @author IBM, Original contribution. 
 * @author Michal Chmielewski (michal.chmielewski@oracle.com)
 * @date Jun 4, 2007
 * 
 */

@SuppressWarnings("nls")
public class TransferBuffer {

	protected static final boolean DEBUG = false;
		
	static final String NL = System.getProperty("line.separator");
	
	/**
	 * @author IBM, Original contribution.
	 * @author Michal Chmielewski (michal.chmielewski@oracle.com)
	 * @date Jun 4, 2007
	 *
	 */
	public class Contents {
		
		Map<EObject, EObject> fExtensionMap;
				
		/** The list of root objects in our transfer buffer */
		List<EObject> fRootObjects;
		
		/** The textual content of the clipboard */
		String fText;
		
		Contents (Map<EObject, EObject> extensionMap, List<EObject> rootList ) {			
			fExtensionMap = extensionMap;
			fRootObjects = rootList;					
		}
		
		@SuppressWarnings("nls")
		void transferToClipboard () {						
			// Populate the clipboard with the XML version of this object list.
			
			if (fRootObjects.size() == 1) {				
				EObject ref = fRootObjects.get(0);				
				fText = fWriter.toXML ( ref );							
				
			} else if (fRootObjects.size() > 1) {
				StringBuilder builder = new StringBuilder();
				builder.append("<bag>").append("\n\n");			
				for(EObject obj : fRootObjects) {								
					builder.append(fWriter.toXML ( obj ));
				}
				builder.append("\n</bag>");
				fText = builder.toString();				
			}
						
			// System.out.println("BPEL Source: " + fClipboardText);
		
			/** Clipboard clipboard = new Clipboard(display);
			 *	String textData = "Hello World";
			 *	String rtfData = "{\\rtf1\\b\\i Hello World}";
			 *	TextTransfer textTransfer = TextTransfer.getInstance();
			 *	RTFTransfer rtfTransfer = RTFTransfer.getInstance();
			 *	Transfer[] transfers = new Transfer[]{textTransfer, rtfTransfer};
			 *	Object[] data = new Object[]{textData, rtfData};
			 *	clipboard.setContents(data, transfers, DND.CLIPBOARD);
			 *	clipboard.dispose();
			 *
			 * Object[] data, Transfer[] dataTypes
			 */		
			fClipboard.setContents(new Object[] { fText } , new Transfer[] { TextTransfer.getInstance() }) ;
		}
		
	}

	/** The system clipboard */
	Clipboard fClipboard = null;
	
	/** The model BPEL Reader that we use to read the content from a clipboard (text paste) */
	org.eclipse.bpel.model.resource.BPELReader fReader = null;	
	
	/** The model BPEL Writer that we use to serialize the content */
	BPELWriter fWriter = null;
	
	/** The current contents of the transfer buffer */
	Contents fContents;
		
	/** The target resource */
	BPELResource fTargetResource;
	
	
	/**
	 * Brand new shiny transfer buffer with clipboard support.
	 * 
	 * @param display
	 */
	
	public TransferBuffer ( Display display ) {	
		fClipboard = new Clipboard(display);
		
		fWriter = new BPELWriter() {
			@Override
			public BPELResource getResource() {
				return fTargetResource;
			}			
		};
		
		fReader = new org.eclipse.bpel.model.resource.BPELReader () {
			@Override
			public Resource getResource() {
				return fTargetResource;
			}			
		};
		
		
	}
	
	
	/**
	 * Return the contents of the transfer buffer.
	 * 
	 * @return contents of the transfer buffer.
	 */

	public Contents getContents() {
		return fContents;
	}

	/**
	 * Set the contents of this transfer buffer.
	 * 
	 * This happens from the BPEL editor side, that is, when the Copy command is issued.
	 * 
	 * @param aContents  contents of this transfer buffer.
	 */

	@SuppressWarnings("nls")
	void setContents (Contents aContents) {		
		fContents = aContents;
	}

	
	String getClipboardText () {			
		return (String) fClipboard.getContents( TextTransfer.getInstance() );		
	}
	
	/**
	 * Get the list of outermost objects from the list of objects passed.
	 * If a sequence and an activity in the sequence are present in aList, then only
	 * the sequence is returned.
	 * 
	 * @param aList
	 * @return the list of outermost objects.
	 */
	
	List<EObject> getOutermostObjects(List<EObject> aList) {

		ArrayList<EObject> trimmedList = new ArrayList<EObject>(aList.size());

		for (EObject next : aList) {
			boolean skipNext = false;
			for (EObject parent : aList) {
				if (next != parent
						&& ModelHelper.isChildContainedBy(parent, next)) {
					skipNext = true;
					break;
				}
			}

			if (skipNext) {
				continue;
			}

			trimmedList.add(next);
		}
		return trimmedList;
	}

	
	
	
	/**
	 * Copy the passed source objects to the transfer buffer.
	 * 
	 * 
	 * @param sourceObjects
	 * @param sourceMap
	 */
	
	public void copyObjectsToTransferBuffer (List<EObject> sourceObjects, ExtensionMap sourceMap) {
		if (DEBUG) {
			System.out
					.println("copyObjectsToTransferBuffer(" + sourceObjects.size() + ")"); //$NON-NLS-1$ //$NON-NLS-2$
		}
		
		Map<EObject, EObject> targetMap = new HashMap<EObject, EObject>();
		List<EObject> sourceList = getOutermostObjects (sourceObjects);
		List<EObject> targetList = new ArrayList<EObject>();
		
		
		// TODO: are there issues here with processing subtrees one-by-one?
		// E.g. references
		// to an object which is copied in a different subtree? (Probably not,
		// for our model)
		
		for (EObject source : sourceList) {
			EObject target = BPELUtil
					.cloneSubtree(source, sourceMap, targetMap).targetRoot;
			targetList.add(target);		
		}

		
		// Remove links which are referenced by root activities and were not
		// copied! Otherwise, when the root activities are pasted the stale
		// references from
		// roots to these links will be pasted too.
		
		for ( EObject next : targetList ) {
			if (next instanceof Activity == false) {
				continue;
			}
			
			Activity activity = (Activity) next;
			
			Sources sources = activity.getSources();
			if (sources != null) {
				
				for(Source source : sources.getChildren() ) {
					if (!targetMap.containsValue(source.getLink())) {
						source.setLink(null);
						sources.getChildren().remove(source);						
					}
				}
				if (sources.getChildren().isEmpty()) {
					activity.setSources(null);
				}
			}

			Targets targets = activity.getTargets();
			if (targets != null) {
				for (Target target : targets.getChildren() ) {
					if (!targetMap.containsValue(target.getLink())) {
						target.setLink(null);
						targets.getChildren().remove(target);
					}
				}
				if (targets.getChildren().isEmpty()) {
					activity.setTargets(null);
				}
			}
		}

		/**
		 * This has to have a better way of being computed.
		 * The transfer buffer belongs to the editor and an editor edits one resource. So this should be an invariant 
		 * over the lifetime of the transfer buffer.
		 * 
		 */
		
		if (fTargetResource == null) {
			fTargetResource = (BPELResource) ModelHelper.getBPELEditor( sourceList.get(0) ).getResource();
		}		
		
		Contents contents = new Contents( targetMap,targetList );
		contents.transferToClipboard();
		
		setContents( contents );
	}

	/**
	 * Copy the transfer buffer to the targetObject.
	 * 
	 * @param targetObject
	 *            the target object to act as an anchor point.
	 * 
	 * @param targetMap
	 * @param bReference treat target as reference.
	 * 
	 * @return the list of new objects added.
	 */

	@SuppressWarnings("nls")
	public List<EObject> copyTransferBuffer (EObject targetObject,
			ExtensionMap targetMap, boolean bReference ) {

		String xml = getClipboardText();

		// First check if we need to copy from clipboard.
		
		if (fContents == null || xml.equals(fContents.fText) == false ) {
			
			if (couldBeXML(xml) == false) {
				setContents( null );
			} else {
				if (fTargetResource == null) {
					// This has to have a better way of being computed.
					fTargetResource = (BPELResource) ModelHelper.getBPELEditor( targetObject ).getResource();
				}
	
				List<EObject> result = fReader.fromXML( adjustXMLSource(xml) , "Clipboard", fTargetResource );
				if (result.size() > 0) {
					setContents( new Contents(null,result) );
				} else {
					setContents( null );
				}
			} 
		}
		
		Anchors anchors = getAnchors (targetObject,bReference,fContents);		
		
		return copyContentsTo (fContents, anchors, targetMap);				
	}
	
	
	
	List<EObject> copyContentsTo ( Contents contents, Anchors anchors, ExtensionMap targetMap ) {
	
		ArrayList<EObject> newObjects = new ArrayList<EObject>();
		
		if (contents == null) {
			return newObjects;
		}
		
		for (EObject source : contents.fRootObjects) {

			BPELUtil.CloneResult cloneResult = BPELUtil.cloneSubtree(source,contents.fExtensionMap, targetMap);
						
			anchors.fContainer.addChild(anchors.fTarget, cloneResult.targetRoot, anchors.fRefObject );
			// Resolve name of the source activity to be unique
			if (source instanceof Activity) {
				Activity node = (Activity) cloneResult.targetRoot;								
				String uniqueName = BPELUtil.generateUniqueModelName ( anchors.fTarget, node.getName(), node );				
				node.setName(BPELUtil.upperCaseFirstLetter (uniqueName) );
			} else if (source instanceof Variable) {
				Variable node = (Variable) cloneResult.targetRoot;								
				String uniqueName = BPELUtil.generateUniqueModelName ( anchors.fTarget, node.getName(), node );
				node.setName(uniqueName);				
			} else if (source instanceof PartnerLink) {
				PartnerLink node = (PartnerLink) cloneResult.targetRoot;								
				String uniqueName = BPELUtil.generateUniqueModelName ( anchors.fTarget, node.getName(), node );
				node.setName(uniqueName);								
			} else if (source instanceof CorrelationSet ) {
				CorrelationSet node = (CorrelationSet) cloneResult.targetRoot;								
				String uniqueName = BPELUtil.generateUniqueModelName (anchors.fTarget, node.getName(), node );
				node.setName(uniqueName);								
			}
			newObjects.add(cloneResult.targetRoot);
		}

		return newObjects;
	}

	
	class Anchors {
		EObject fTarget;
		EObject fRefObject;
		IContainer<EObject> fContainer;
	}

	/**
	 * Anchors just represent the anchor points for the paste operation.
	 *  
	 * There are several possibilities to consider.
	 *   
	 * <ol>
	 * <li> The  
	 */
	
	Anchors getAnchors ( EObject targetObject, boolean bReference, Contents contents ) {

		Anchors anchors = new Anchors();		
		
		
		anchors.fTarget = targetObject;		
		anchors.fContainer = BPELUtil.adapt(anchors.fTarget, IContainer.class);
			
		/**
		 * If we are not a container, then we presume that a container is our parent
		 * (such an an activity and its container being say a sequence).
		 * 
		 * Also, if bReference is forced, then were a reference object (the insertion point)
		 * and so we must go to our container.
		 */
		
		if (anchors.fContainer == null || bReference ) {
			
			// check its container
			anchors.fRefObject = targetObject;
			anchors.fTarget  = targetObject.eContainer();
			anchors.fContainer = BPELUtil.adapt(anchors.fTarget, IContainer.class);
			
		} else {
		
			/** Otherwise we are container.
			 *  
			 * But can we take the contents that is given to us ?
			 */ 
			
			if (canCopyContents(anchors, contents) == true) {
				return anchors;
			}
			
			/** Otherwise we assume that we are referenced container*/
			anchors.fRefObject = targetObject;
			anchors.fTarget    = targetObject.eContainer();
			anchors.fContainer = BPELUtil.adapt(anchors.fTarget, IContainer.class);
			
		}
		return anchors;		
	}

	
	boolean canCopyContents ( Anchors anchors, Contents content ) {
		
		if (content == null) {
			return false;
		}
		
		// check each root object's type against the container..
		for (EObject next : content.fRootObjects ) {				
			if (anchors.fContainer.canAddObject(anchors.fTarget, next, anchors.fRefObject) ) {
				return true;
			}
		}
		
		return false;
	}
	
	
	/**
	 * 
	 * 
	 * @param targetObject
	 *            the target reference object around which the copy-from-buffer
	 *            should be made.
	 * @param bReference - treat the target as a reference, even if it is a container.
	 * @return true of copy of transfer buffer can be made, false otherwise.
	 */

	@SuppressWarnings("nls")
	public boolean canCopyTransferBufferTo (EObject targetObject, boolean bReference  ) {
		if (targetObject == null ) {
			return false;
		}
		
		Anchors anchors = getAnchors (targetObject,bReference, fContents );
				
		if (anchors.fContainer == null) {
			return false;
		}		
		
		return canCopyContents ( anchors, fContents ) || couldBeXML(getClipboardText()) ;
	}

	

	
	
	/**
	 * This is a completely heuristic test to see if we have something in XML in the clipboard
	 * that "could" be converted into objects in our little world.
	 * 
	 * 
	 * 
	 * @param xml
	 * @return
	 */
	boolean couldBeXML (String xml) {
		
		if (xml == null || xml.length() < 4) {
			return false;
		}
		// Short valid XML element would be .... ?
		// <f/>
		
		int nOpen = 0;
		int nClose = 0;
		int nNonWhitespace = 0;
		int nWhitespace = 0;
		
		/** open and close should match, we count them, and then make sure we have about 0.95 ratio */
		for (char ch : xml.toCharArray() ) {
			
			if (Character.isWhitespace(ch)) {
				nWhitespace += 1;
			} else if (ch == '<') {
				// leading non-whitespace 
				if (nOpen == 0 && nNonWhitespace > 0) {
					return false;
				}
				nOpen += 1;
			} else if (ch == '>') {
				nClose += 1;
			} else {
				nNonWhitespace += 1;
			}
			
		}
		
		if (nOpen == 0 || nClose == 0) {
			return false;
		}
		
		float ratio = (float) (Math.min(nOpen, nClose) * 1.0 / 1.0 * Math.max(nOpen,nClose));
		
		if (ratio < 0.95) {
			return false;
		}

		return true;
	}
	
	
	/** Any namespace declaration, either xmlns="" or pfx:xmlns="xx" */
	static Pattern anyNamespace = Pattern.compile("(\\:|\\s)xmlns=(\\\"|\\')", Pattern.MULTILINE );
	
	/** BPEL 2004 namespace, as default (no prefix mapping) */
	static Pattern bpel2004DefaultNS = Pattern.compile("\\sxmlns=(\"|\')" + Pattern.quote(BPELConstants.NAMESPACE_2004) + "(\"|\')",
			Pattern.MULTILINE);
	
	/** BPEL 2003 namespace, as default (no prefix mapping) */
	static Pattern bpel2003DefaultNS = Pattern.compile("\\sxmlns=(\"|\')" + Pattern.quote(BPELConstants.NAMESPACE_2003) + "(\"|\')",
			Pattern.MULTILINE);
	
	static String EMPTY_STRING = "";
	
	 
	/**
	 * Adjust XML source so that we can parse it as BPEL 2.0 
	 * 
	 * @param buffer the current XML source.
	 * @return the adjust source.
	 */
	
	static public String adjustXMLSource ( String buffer ) {
		 
		/** Check if no Namespaces at all. Then there are no prefixes that are namespace bound */
		Matcher matcher = anyNamespace.matcher(buffer);
		
		if (matcher.find() == false) {
			StringBuilder sb = new StringBuilder(buffer.length() + 128);
			sb.append("<bag xmlns=\"").append( BPELConstants.NAMESPACE ).append("\">").append(NL);
			sb.append(buffer);			
			sb.append(NL).append("</bag>");
			return sb.toString();
			
		}
		
		/** Check if there is a BPELNamespace source mapping in the buffer. */

		/** Check pre-2.0 namespaces as well and replace them by the 2.0 BPEL namespace */
		
		matcher = bpel2004DefaultNS.matcher(buffer);
		if ( matcher.find() ) {			
			return adjustXMLSource(matcher.replaceAll( EMPTY_STRING )) ;
		}
		
		matcher = bpel2003DefaultNS.matcher(buffer);
		if ( matcher.find() ) {			
			return adjustXMLSource ( matcher.replaceAll( EMPTY_STRING ) );			
		}
		
		/** Return buffer as is */
		return buffer;
		
	}
	/**
	 * 
	 */
	
	public void dispose() {
		setContents(null);
		
		if (fClipboard != null) {
			fClipboard.dispose();
			fClipboard = null;
		}		
	}


}
