/*******************************************************************************
 * Copyright (c) 2001, 2010 IBM Corporation and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License 2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 * 
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *     Jens Lukowski/Innoopract - initial renaming/restructuring
 *     
 *******************************************************************************/
package org.eclipse.wst.xml.ui.internal.dnd;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.List;
import java.util.Vector;

import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.Path;
import org.eclipse.jface.viewers.StructuredSelection;
import org.eclipse.jface.viewers.TreeViewer;
import org.eclipse.swt.dnd.DND;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.wst.common.ui.internal.dnd.DefaultDragAndDropCommand;
import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel;
import org.eclipse.wst.xml.core.internal.provisional.document.IDOMNode;
import org.eclipse.wst.xml.ui.internal.Logger;
import org.eclipse.wst.xml.ui.internal.XMLUIMessages;
import org.w3c.dom.Attr;
import org.w3c.dom.DOMException;
import org.w3c.dom.Element;
import org.w3c.dom.Node;

public class DragNodeCommand extends DefaultDragAndDropCommand {
	private List fSelections;
	private TreeViewer fTreeViewer;

	public DragNodeCommand(Object target, float location, int operations, int operation, Collection sources, TreeViewer treeViewer) {
		super(target, location, operations, operation, sources);
		fTreeViewer = treeViewer;
		fSelections = new ArrayList();
	}

	private void beginModelChange(Node node, boolean batchUpdate) {
		IStructuredModel structuredModel = getStructuredModel(node);
		if (structuredModel != null) {
			String undoDesc = new String();
			if (getOperation() == DND.DROP_MOVE) {
				undoDesc = XMLUIMessages.DragNodeCommand_0;
			}
			else if (getOperation() == DND.DROP_COPY) {
				undoDesc = XMLUIMessages.DragNodeCommand_1;
			}

			structuredModel.beginRecording(this, undoDesc);
			if (batchUpdate) {
				// structuredModel.aboutToChangeModel();
			}
		}
	}

	public boolean canExecute() {
		return executeHelper(true);
	}

	private boolean doModify(Node source, Node parentNode, Node refChild, boolean testOnly) {
		boolean result = false;
		if (source.getNodeType() == Node.ATTRIBUTE_NODE) {
			Attr sourceAttribute = (Attr) source;
			Element sourceAttributeOwnerElement = sourceAttribute.getOwnerElement();
			if ((parentNode.getNodeType() == Node.ELEMENT_NODE) && (sourceAttributeOwnerElement != parentNode)) {
				result = true;
				if (!testOnly) {
					try {
						if (getOperation() == DND.DROP_MOVE) {
							Element targetElement = (Element) parentNode;
							sourceAttributeOwnerElement.removeAttributeNode(sourceAttribute);
							targetElement.getAttributes().setNamedItem(sourceAttribute);
							fSelections.add(sourceAttribute);
							}
						else if (getOperation() == DND.DROP_COPY) {
							Attr cloneAttribute = (Attr) sourceAttribute.cloneNode(false);
							Element targetElement = (Element) parentNode;
							targetElement.getAttributes().setNamedItem(cloneAttribute);
							fSelections.add(cloneAttribute);
						}
					}
					catch (Exception e) {
						Logger.log(Logger.WARNING_DEBUG, e.getMessage(), e);
					}
				}
			}
		}
		else {
			if (((parentNode.getNodeType() == Node.ELEMENT_NODE) || (parentNode.getNodeType() == Node.DOCUMENT_NODE)) && !(refChild instanceof Attr)) {
				result = true;

				if (!testOnly) {
					try {
						if (isAncestor(source, parentNode)) {
							// System.out.println("can not perform this drag drop
							// operation.... todo... pop up dialog");
						}
						else {
							// defect 221055 this test is required or else the
							// node will
							// be removed from the tree and the insert will fail
							if (source != refChild) {
								if (getOperation() == DND.DROP_MOVE) {
									Node oldParent = source.getParentNode();
									Node oldSibling = source.getNextSibling();
									oldParent.removeChild(source);
									try {
										parentNode.insertBefore(source, refChild);
									} catch (DOMException e) {
										// bug151692 - if unable to move node to new location, reinsert back to old location
										oldParent.insertBefore(source, oldSibling);
									}
									fSelections.add(source);
								}
								else if (getOperation() == DND.DROP_COPY) {
									Node nodeClone = source.cloneNode(true);
									parentNode.insertBefore(nodeClone, refChild);
									fSelections.add(nodeClone);
								}
							}
						}
					}
					catch (Exception e) {
						Logger.log(Logger.WARNING_DEBUG, e.getMessage(), e);
					}
				}
			}
		}
		return result;
	}

	private void endModelChange(Node node, boolean batchUpdate) {
		IStructuredModel structuredModel = getStructuredModel(node);
		if (structuredModel != null) {
			structuredModel.endRecording(this);
			if (batchUpdate) {
				// structuredModel.changedModel();
			}
		}
	}

	public void execute() {
		executeHelper(false);

		// Make our selection if the treeViewer != null
		if (fTreeViewer != null) {
			StructuredSelection structuredSelection = new StructuredSelection(fSelections);
			fTreeViewer.setSelection(structuredSelection);
		}
	}

	private boolean executeHelper(boolean testOnly) {
		boolean result = true;
		if (target instanceof Node) {
			Node targetNode = (Node) target;
			if (!testOnly && fTreeViewer != null && !validateEdit(getStructuredModel(targetNode), fTreeViewer.getControl().getShell()))
				return false;

			Node parentNode = getParentForDropPosition(targetNode);
			Node refChild = getRefChild(targetNode);

			Vector sourcesList = new Vector();
			sourcesList.addAll(sources);

			removeMemberDescendants(sourcesList);
			boolean performBatchUpdate = sourcesList.size() > 5;

			if (!testOnly) {
				beginModelChange(targetNode, performBatchUpdate);
			}
			for (Iterator i = sourcesList.iterator(); i.hasNext();) {
				Object source = i.next();
				if (source instanceof Node) {
					if (!((refChild == null) && (targetNode instanceof Attr)) && (parentNode != null)) {
						result = doModify((Node) source, parentNode, refChild, testOnly);
					}
					else {
						result = false;
					}
					if (!result) {
						break;
					}
				}
			}
			if (!testOnly) {
				endModelChange(targetNode, performBatchUpdate);
			}
		}
		else {
			result = false;
		}
		return result;
	}

	/**
	 * Checks that the resource backing the model is writeable utilizing <code>validateEdit</code>
	 * on a given <tt>IWorkspace</tt>.
	 * 
	 * @param model the model to be checked
	 * @param context the shell context for which <code>validateEdit</code> will be run
	 * @return boolean result of checking <code>validateEdit</code>. If the resource is unwriteable, <code>status.isOK()</code>
	 * will return true; otherwise, false.
	 */
	private boolean validateEdit(IStructuredModel model, Shell context) {
		if (model != null && model.getBaseLocation() != null) {
			IFile file = ResourcesPlugin.getWorkspace().getRoot().getFile(new Path(model.getBaseLocation()));
			return !file.isAccessible() || ResourcesPlugin.getWorkspace().validateEdit(new IFile[] {file}, context).isOK();
		}
		return false; //$NON-NLS-1$
	}

	public int getFeedback() {
		int result = DND.FEEDBACK_SELECT;
		if (location > 0.75) {
			result = DND.FEEDBACK_INSERT_AFTER;
		}
		else if (location < 0.25) {
			result = DND.FEEDBACK_INSERT_BEFORE;
		}
		return result;
	}

	private Node getParentForDropPosition(Node node) {
		Node result = null;

		int feedback = getFeedback();
		if (feedback == DND.FEEDBACK_SELECT) {
			result = node;
		}
		else {
			result = getParentOrOwner(node);
		}
		return result;
	}


	private Node getParentOrOwner(Node node) {
		return (node.getNodeType() == Node.ATTRIBUTE_NODE) ? ((Attr) node).getOwnerElement() : node.getParentNode();
	}


	private Node getRefChild(Node node) {
		Node result = null;

		int feedback = getFeedback();

		if (feedback == DND.FEEDBACK_INSERT_BEFORE) {
			result = node;
		}
		else if (feedback == DND.FEEDBACK_INSERT_AFTER) {
			result = node.getNextSibling();
		}
		return result;
	}

	private IStructuredModel getStructuredModel(Node node) {
		IStructuredModel result = null;
		if (node instanceof IDOMNode) {
			result = ((IDOMNode) node).getModel();
		}
		return result;
	}

	// returns true if a is an ancestore of b
	//
	private boolean isAncestor(Node a, Node b) {
		boolean result = false;
		for (Node parent = b; parent != null; parent = parent.getParentNode()) {
			if (parent == a) {
				result = true;
				break;
			}
		}
		return result;
	}


	/**
	 * This method removes members of the list that have ancestors that are
	 * also members of the list.
	 */
	private void removeMemberDescendants(List list) {
		Hashtable table = new Hashtable();
		for (Iterator i = list.iterator(); i.hasNext();) {
			Object node = i.next();
			table.put(node, node);
		}

		for (int i = list.size() - 1; i >= 0; i--) {
			Object node =  list.get(i); 
			if (node instanceof Node){
				for (Node parent = getParentOrOwner((Node)node); parent != null; parent = getParentOrOwner(parent)) {
					if (table.get(parent) != null) {
						list.remove(i);
						break;
					}
				}
			}
		}
	}
}
