/*******************************************************************************
 * Copyright (c) 2001, 2011 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
 *******************************************************************************/

package org.eclipse.wst.xml.core.internal.emf2xml;

import java.util.Collection;
import java.util.Iterator;

import org.eclipse.emf.common.notify.Notifier;
import org.eclipse.emf.ecore.EObject;
import org.eclipse.emf.ecore.resource.Resource;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.wst.common.internal.emf.resource.EMF2DOMAdapter;
import org.eclipse.wst.common.internal.emf.resource.EMF2DOMAdapterImpl;
import org.eclipse.wst.common.internal.emf.resource.EMF2DOMRenderer;
import org.eclipse.wst.common.internal.emf.resource.Translator;
import org.eclipse.wst.common.internal.emf.resource.TranslatorResource;
import org.eclipse.wst.common.internal.emf.utilities.Assert;
import org.eclipse.wst.common.internal.emf.utilities.DOMUtilities;
import org.eclipse.wst.common.internal.emf.utilities.FeatureValueConversionException;
import org.eclipse.wst.sse.core.internal.provisional.INodeAdapter;
import org.eclipse.wst.sse.core.internal.provisional.INodeNotifier;
import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocument;
import org.eclipse.wst.xml.core.internal.Logger;
import org.eclipse.wst.xml.core.internal.provisional.document.IDOMElement;
import org.eclipse.wst.xml.core.internal.provisional.document.IDOMModel;
import org.eclipse.wst.xml.core.internal.provisional.document.IDOMNode;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.Text;

public class EMF2DOMSSEAdapter extends EMF2DOMAdapterImpl implements INodeAdapter {
	
	private Class resourceClass;
	public EMF2DOMSSEAdapter(Node node, EMF2DOMRenderer renderer, Translator translator) {
		super(node, renderer, translator);
	}

	public EMF2DOMSSEAdapter(Notifier object, Node node, EMF2DOMRenderer renderer, Translator translator) {
		super(object, node, renderer, translator);
	}

	public EMF2DOMSSEAdapter(TranslatorResource resource, Document document, EMF2DOMRenderer renderer, Translator translator) {
		super(resource, document, renderer, translator);
	}

	protected String calcIndentString(Node node) {
		Assert.isNotNull(node);
		Assert.isNotNull(node.getParentNode(), "Node must be connected into the tree"); //$NON-NLS-1$

		Node parent = node.getParentNode();

		String indentString = getNewlineString(node);

		// Find indentation string for this node based on its sibling or
		// parent
		Node previousSibling = DOMUtilities.getPreviousNodeSibling(node);
		if (previousSibling != null) {
			indentString = primGetIndentString(previousSibling);
		}
		else {
			String parentIndentString = primGetIndentString(parent);
			indentString = parentIndentString + DOMUtilities.INDENT_STRING;
		}
		return indentString;
	}

	/*
	 * Prints out a DOM notification for debugging.
	 */
	protected void debugDOMNotify(INodeNotifier notifier, int eventType, Object changedFeature, Object oldValue, Object newValue) {
		if (fDebug) {
			String notifType = ""; //$NON-NLS-1$
			switch (eventType) {
				case INodeNotifier.ADD :
					notifType = "ADD"; //$NON-NLS-1$
					break;
				case INodeNotifier.REMOVE :
					notifType = "REMOVE"; //$NON-NLS-1$
					break;
				case INodeNotifier.CHANGE :
					notifType = "CHANGE"; //$NON-NLS-1$
					break;
				case INodeNotifier.CONTENT_CHANGED :
					notifType = "CONTENT_CHANGED"; //$NON-NLS-1$
					break;
				case INodeNotifier.STRUCTURE_CHANGED :
					notifType = "STRUCTURE_CHANGE"; //$NON-NLS-1$
					break;
			}
			Logger.log(Logger.INFO_DEBUG, "DOM Change: " + notifType); //$NON-NLS-1$
			Logger.log(Logger.INFO_DEBUG, "\tnotifier      : " + notifier); //$NON-NLS-1$
			Logger.log(Logger.INFO_DEBUG, "\tchangedFeature: " + changedFeature); //$NON-NLS-1$
			Logger.log(Logger.INFO_DEBUG, "\toldValue      : " + oldValue); //$NON-NLS-1$
			Logger.log(Logger.INFO_DEBUG, "\tnewValue      : " + newValue); //$NON-NLS-1$
		}
	}

	protected void disableUndoManagementIfNecessary() {
		IDOMModel model = getXMLModel();
		if (model != null && model.getUndoManager() != null)
			model.disableUndoManagement();
	}

	protected void enableUndoManagement() {
		IDOMModel model = getXMLModel();
		if (model != null && model.getUndoManager() != null)
			model.enableUndoManagement();
	}

	protected String getNewlineString(Node node) {
		/*
		 * We should always have IDOMNode, and IStructuredDocument, and
		 * consquently a valid "preferred" line delimiter, but just to be
		 * safe, we'll assign something by default.
		 */
		if (node instanceof IDOMNode) {
			IDOMNode xmlNode = (IDOMNode) node;
			IStructuredDocument document = xmlNode.getStructuredDocument();
			if (document != null) {
				return document.getLineDelimiter();
			}
		}
		return DOMUtilities.NEWLINE_STRING;
	}

	protected IDOMModel getXMLModel() {
		if (getNode() != null)
			return ((IDOMNode) getNode()).getModel();
		return null;
	}

	protected IDOMNode getXMLNode() {
		return (IDOMNode) getNode();
	}

	/*
	 * Do nothing for SSE, we will tolerate anything they add
	 */
	protected void handleFeatureValueConversionException(FeatureValueConversionException ex) {
		// Do nothing
	}

	/*
	 * Do nothing for SSE, we will tolerate anything they add
	 */
	protected void handleInvalidMultiNodes(String nodeName) {
		// Do nothing
	}

	protected void indent(Node node, Translator map) {
		Assert.isNotNull(node.getParentNode(), "Node must be connected into the tree"); //$NON-NLS-1$
		Assert.isNotNull(node);

		String indentString = calcIndentString(node);

		// Indent before the start tag
		indentStartTag(indentString, node, map);

		// Indent before the end tag
		indentEndTag(indentString, node, map);
	}

	/**
	 * Indent before the end tag of the <node>passed in.
	 */
	protected void indentEndTag(String indentString, Node node, Translator map) {
		if (!map.shouldIndentEndTag(node))
			return;
		String domPath = map.getDOMPath();

		if ((!map.isManagedByParent() && !map.isDOMTextValue()) || (map.isManagedByParent() && domPath.length() != 0) && node.getNodeName().equals(domPath)) {
			Text newWS = node.getOwnerDocument().createTextNode(getNewlineString(node) + indentString); //$NON-NLS-1$
			DOMUtilities.insertBeforeNode(node, newWS, null);
		}
	}

	/**
	 * Indent before the start tag of the <node>passed in.
	 */
	protected void indentStartTag(String indentString, Node node, Translator map) {
		Node parent = node.getParentNode();
		Text newWS = node.getOwnerDocument().createTextNode(getNewlineString(node) + indentString); //$NON-NLS-1$
		DOMUtilities.insertAfterNode(parent, newWS, DOMUtilities.getPreviousNodeSibling(node));
	}

	protected boolean isEmptyTag(Element parent) {
		return ((IDOMElement) parent).isEmptyTag();
	}

	/*
	 * This method is called when the DOM node changes. It attempts to update
	 * MOF object based on the changes.
	 */
	public void notifyChanged(INodeNotifier notifier, int eventType, Object changedFeature, Object oldValue, Object newValue, int pos) {

		if (!isNotificationEnabled())
			return;

		debugDOMNotify(notifier, eventType, changedFeature, oldValue, newValue);

		if (notifier != getNode() && eventType != INodeNotifier.CHANGE) {
			// This is the case where the notification was sent from a
			// sub node. Use the notifiers name to determine which
			// MOF feature to update. Note that is is assumed that if
			// the eventType is CHANGE then it attribute on a path node
			// changing. This was put in for the EGL group.
			if (notifier instanceof Element) {
				if (eventType == INodeNotifier.STRUCTURE_CHANGED || eventType == INodeNotifier.CONTENT_CHANGED || eventType == INodeNotifier.CHANGE) {
					Element notifyingNode = (Element) notifier;
					Translator map = findTranslator(notifyingNode.getNodeName(), false);
					if (map != null)
						updateMOFFeature(map, getNode(), getEObject());
				}
			}
		}
		else {
			// Update everything on STRUCTURE_CHANGE or CONTENT_CHANGE.
			// Other event types occur too often.
			if (eventType == INodeNotifier.STRUCTURE_CHANGED || eventType == INodeNotifier.CONTENT_CHANGED) {
				updateMOF();
			}
			// Update just the attribute that changed.
			else if (eventType == INodeNotifier.CHANGE) {
				Translator map = findTranslator(changedFeature.toString(), true);
				if (map != null)
					updateMOFFeature(map, getNode(), getEObject());
			}
		}
	}

	protected void postUpdateDOMFeature(Translator map, Node node, EObject mofObject) {
		enableUndoManagement();
	}

	protected void preUpdateDOMFeature(Translator map, Node node, EObject mofObject) {
		super.preUpdateDOMFeature(map, node, mofObject);
		disableUndoManagementIfNecessary();
	}

	protected void primAddDOMAdapter(Node aNode, EMF2DOMAdapter anAdapter) {
		((IDOMNode) aNode).addAdapter((EMF2DOMSSEAdapter) anAdapter);
	}

	/**
	 * Create an adapter for a child DOM node
	 * 
	 * @param node
	 *            org.w3c.dom.Node The node to create the adapter for.
	 */
	protected EMF2DOMAdapter primCreateAdapter(EObject mofObject, Translator childMap) {
		Element newNode = createNewNode(mofObject, childMap);
		return new EMF2DOMSSEAdapter(mofObject, newNode, fRenderer, childMap);
	}

	/**
	 * Create an adapter for a child DOM node
	 * 
	 * @param node
	 *            org.w3c.dom.Node The node to create the adapter for.
	 */
	protected EMF2DOMAdapter primCreateAdapter(Node node, Translator childMap) {
		return new EMF2DOMSSEAdapter(node, fRenderer, childMap);
	}
	protected EMF2DOMAdapter createAdapter(Node node, Translator childMap) {

		//Assert.isNotNull(childMap.getChildAdapterClass());
		Assert.isNotNull(node);

		EMF2DOMAdapter adapter = primGetExistingAdapter(node,childMap);

		if (adapter != null) {
			if (adapter.isMOFProxy() || adapter.getTarget() == null) {
				removeDOMAdapter(node, adapter);
				if (adapter.getTarget() != null) {
					adapter.getTarget().eAdapters().remove(adapter);
				}
				adapter = null;
			}
		}
		if (adapter == null) {
			adapter = primCreateAdapter(node, childMap);
		}
		return adapter;
	}
	protected void addDOMAdapter(Node childNode) {

		// Only add the adapter if this is an child node that will not be
		// adapted. For instance a subtree that maps to a MOF attribute
		// setting.
		if (childNode.getNodeType() == Node.ELEMENT_NODE) {
			EMF2DOMAdapter attrAdapter = primGetExistingAdapter(childNode);

			if (attrAdapter == null || attrAdapter.getNode() != getNode()) {
				// If the node is adapted, but not by this adapter then remove
				// it. This happens to non-object children when the parent tag
				// name is changed.
				removeDOMAdapter(childNode, attrAdapter);

				if (fDebug) {
					org.eclipse.jem.util.logger.proxy.Logger.getLogger().logError("\tCHILD: Adding DOM adapter: " + this); //$NON-NLS-1$
					org.eclipse.jem.util.logger.proxy.Logger.getLogger().logError("\t\tto: " + childNode); //$NON-NLS-1$
				}
				primAddDOMAdapter(childNode, this);
			}
		}
	}
	
	protected EMF2DOMAdapter primGetExistingAdapter(Node aNode, Translator childMap) {
		INodeNotifier sseNode = (INodeNotifier) aNode;
		Collection adapters = sseNode.getAdapters();
		for (Iterator iterator = adapters.iterator(); iterator.hasNext();) {
			INodeAdapter adapter = (INodeAdapter) iterator.next();
			// First Check if it's an EMF2DOMAdapter
			if (adapter != null && adapter.isAdapterForType(EMF2DOMAdapter.ADAPTER_CLASS)) {
				// Cast to EMF2DOMAdapter
				EMF2DOMSSEAdapter e2DAdapter = (EMF2DOMSSEAdapter) adapter;
				Object adapterTarget = e2DAdapter.getTarget();
				//Translator adapterTranslator = e2DAdapter.fTranslator;
				
				//Handle the cases where either adapter's target is null 
				if ((getTarget() == null) || (adapterTarget == null))
					if (resourceClass != null && resourceClass.equals(e2DAdapter.getResourceClass()))
						return e2DAdapter;
					else
						continue;
				
				
				// Check if target is an EMF resource
				if (getTarget() instanceof Resource) {
					/*
					 * Now check if it's the right one (Multiple resources
					 * could be attached)
					 */
					if (adapterTarget != null && adapterTarget == getTarget()) {
						return e2DAdapter;
					}
				}
				else {
					// Check if targets are EObjects with the same resources
					EObject myTarget = (EObject) getTarget();
					/*
					 * Now check if it's the right one (Multiple resources could
					 * be attached)
					 */
					if (adapterTarget != null && ((EObject)adapterTarget).eResource() == myTarget.eResource()) {
						return e2DAdapter;
					}
				}
				
				if (adapterTarget instanceof EObject) {
					if (((EObject) e2DAdapter.getTarget()).eResource() == null && e2DAdapter.fTranslator == childMap) {
						return e2DAdapter;
					}
				}
			}
		}
		return null;
	}
	
	protected EMF2DOMAdapter primGetExistingAdapter(Node aNode) {
		INodeNotifier sseNode = (INodeNotifier) aNode;
		Collection adapters = sseNode.getAdapters();
		for (Iterator iterator = adapters.iterator(); iterator.hasNext();) {
			INodeAdapter adapter = (INodeAdapter) iterator.next();
			// First Check if it's an EMF2DOMAdapter
			if (adapter != null && adapter.isAdapterForType(EMF2DOMAdapter.ADAPTER_CLASS)) {
				// Cast to EMF2DOMAdapter
				EMF2DOMSSEAdapter e2DAdapter = (EMF2DOMSSEAdapter) adapter;
				Object adapterTarget = e2DAdapter.getTarget();
				
				//Handle the cases where either adapter's target is null 
				if ((getTarget() == null) || (adapterTarget == null))
					if (resourceClass != null && resourceClass.equals(e2DAdapter.getResourceClass()))
						return e2DAdapter;
					else
						continue;
				
				
				// Check if target is an EMF resource
				if (getTarget() instanceof Resource) {
					/*
					 * Now check if it's the right one (Multiple resources
					 * could be attached)
					 */
					if (adapterTarget != null && adapterTarget == getTarget()) {
						return e2DAdapter;
					}
				}
				else {
					// Check if targets are EObjects with the same resources
					EObject myTarget = (EObject) getTarget();
					/*
					 * Now check if it's the right one (Multiple resources could
					 * be attached)
					 */
					if (adapterTarget != null && ((EObject)adapterTarget).eResource() == myTarget.eResource()) {
						return e2DAdapter;
					}
				}
				
				if (adapterTarget instanceof EObject) {
					if (((EObject) e2DAdapter.getTarget()).eResource() == null) {
						return e2DAdapter;
					}
				}
			}
		}
		return null;
	}

	protected String primGetIndentString(Node node) {
		IStructuredDocument flatModel = ((IDOMNode) node).getStructuredDocument();
		int nodeStartOff = ((IDOMNode) node).getStartOffset();

		int startOff = Math.max(0, nodeStartOff - 100);
		int endOff = nodeStartOff;

		if(flatModel != null) {
			try {
				String text = flatModel.get(startOff, endOff - startOff);
	
				int inx = text.length() - 1;
				if (inx >= 0) {
					for (; inx >= 0; inx--) {
						char ch = text.charAt(inx);
						if (Character.isWhitespace(ch) && ch != '\n' && ch != '\r') {
							continue;
						}
						inx++;
						break;
					}
	
					return text.substring(inx);
				}
			}
			catch (BadLocationException ex) {
				Logger.logException(ex);
			}
		}
		return ""; //$NON-NLS-1$
	}

	protected void removeDOMAdapter(Node aNode, EMF2DOMAdapter anAdapter) {
		((IDOMNode) aNode).removeAdapter((INodeAdapter) anAdapter);
	}


	protected void reorderDOMChild(Node parentNode, Node childNode, Node insertBeforeNode, Translator map) {
		super.reorderDOMChild(parentNode, childNode, insertBeforeNode, map);
		// Since reordering deletes all the whitespace before the node, we
		// must indent .
		if (insertBeforeNode != null && insertBeforeNode.getNodeType() == Node.ELEMENT_NODE)
			indentStartTag(calcIndentString(insertBeforeNode), insertBeforeNode, map);
		else
			indentStartTag(calcIndentString(childNode), childNode, map);
	}

	protected void setEmptyTag(Element element) {
		((IDOMElement) element).setEmptyTag(true);
	}

	public void updateDOM() {
		if (!isNotificationEnabled())
			return;
		try {
			disableUndoManagementIfNecessary();
			primUpdateDOM();
		}
		finally {
			enableUndoManagement();
		}
	}

	public Class getResourceClass() {
		return resourceClass;
	}

	protected void initChildTranslators() {
		if (fRenderer != null && fRenderer.getResource() != null)
			resourceClass = fRenderer.getResource().getClass();
		super.initChildTranslators();
	}

}
