/*******************************************************************************
 * Copyright (c) 2001, 2008 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.core.internal.document;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import org.eclipse.wst.sse.core.internal.provisional.INodeNotifier;
import org.eclipse.wst.sse.core.internal.util.Debug;
import org.eclipse.wst.xml.core.internal.Logger;
import org.eclipse.wst.xml.core.internal.provisional.document.IDOMNode;
import org.w3c.dom.Attr;
import org.w3c.dom.Element;
import org.w3c.dom.Node;


public class XMLModelNotifierImpl implements XMLModelNotifier {

	private static class NotifyEvent {
		Object changedFeature;
		boolean discarded;
		Object newValue;
		// note: don't initialize instance variables, since
		// that causes double assignments, and lots of these are created.
		INodeNotifier notifier;
		Object oldValue;
		int pos;
		String reason;
		int type;
		int index;

		NotifyEvent(INodeNotifier notifier, int type, Object changedFeature, Object oldValue, Object newValue, int pos) {
			this.notifier = notifier;
			this.type = type;
			this.changedFeature = changedFeature;
			this.oldValue = oldValue;
			this.newValue = newValue;
			this.pos = pos;
			this.reason = ""; //$NON-NLS-1$
		}
	}

	private final static String ADDED_THEN_REMOVED = "Discard: Added then removed rule"; //$NON-NLS-1$
	private final static boolean fOptimizeDeferred = true;
	private final static boolean fOptimizeDeferredAccordingToParentAdded = true;
	private final static boolean fOptimizeDeferredAccordingToParentRemoved = true;
	private final static String PARENT_IS_ADDED = "Disarded: Parent has just been added"; //$NON-NLS-1$
	/* start: for debugging only */
	private final static String PARENT_IS_REMOVED_TOO = "Discard: Parent was removed too"; //$NON-NLS-1$
	private final static String PARENT_IS_REPARENTED = "Not Discard: Parent was removed so this implies reparenting"; //$NON-NLS-1$
	private Node changedRoot = null;

	private boolean changing = false;
	private boolean doingNewModel = false;
	private List fEvents = null;
	private boolean flushing = false;

	/**
	 */
	public XMLModelNotifierImpl() {
		super();
	}

	/**
	 * attrReplaced method
	 * 
	 * @param element
	 *            org.w3c.dom.Element
	 * @param newAttr
	 *            org.w3c.dom.Attr
	 * @param oldAttr
	 *            org.w3c.dom.Attr
	 */
	public void attrReplaced(Element element, Attr newAttr, Attr oldAttr) {
		if (element == null)
			return;
		Attr attr = null;
		String oldValue = null;
		String newValue = null;
		if (oldAttr != null) {
			attr = oldAttr;
			oldValue = oldAttr.getValue();
		}
		if (newAttr != null) {
			attr = newAttr;
			newValue = newAttr.getValue();
		}
		IDOMNode notifier = (IDOMNode) element;
		int offset = notifier.getStartOffset();
		notify(notifier, INodeNotifier.CHANGE, attr, oldValue, newValue, offset);
		propertyChanged(notifier);
	}

	/**
	 */
	public void beginChanging() {
		this.changing = true;
	}

	/**
	 */
	public void beginChanging(boolean newModel) {
		beginChanging();
		this.doingNewModel = newModel;
	}

	public void cancelPending() {
		// we don't want to change the size of this array, since
		// the array may be being processed, in the deferred notification
		// loop, but we can signal that all
		// should be discarded, so any remaining ones will be ignored.
		if (this.fEvents != null) {
			int size = fEvents.size();
			for (int i = 0; i < size; i++) {
				NotifyEvent event = (NotifyEvent) fEvents.get(i);
				event.discarded = true;
			}
		}
		// this cancel is presumably being called as a function of
		// "reinitiailization" so we can ignore changes to the
		// old root, and changes to the new one will be triggered during
		// reinitialization.
		changedRoot = null;
	}

	/**
	 * childReplaced method
	 * 
	 * @param parentNode
	 *            org.w3c.dom.Node
	 * @param newChild
	 *            org.w3c.dom.Node
	 * @param oldChild
	 *            org.w3c.dom.Node
	 */
	public void childReplaced(Node parentNode, Node newChild, Node oldChild) {
		if (parentNode == null)
			return;
		IDOMNode notifier = (IDOMNode) parentNode;
		int type = INodeNotifier.CHANGE;
		if (newChild == null)
			type = INodeNotifier.REMOVE;
		else if (oldChild == null)
			type = INodeNotifier.ADD;
		int offset = notifier.getStartOffset();
		notify(notifier, type, oldChild, oldChild, newChild, offset);
		structureChanged(notifier);
	}

	public void editableChanged(Node node) {
		if (node == null)
			return;
		IDOMNode notifier = (IDOMNode) node;
		int offset = notifier.getStartOffset();
		notify(notifier, INodeNotifier.CHANGE, null, null, null, offset);
		propertyChanged(notifier);
	}

	/**
	 */
	public void endChanging() {
		this.doingNewModel = false;
		if (!this.changing)
			return; // avoid nesting calls
		notifyDeferred();
		if (this.changedRoot != null) {
			notifyStructureChanged(this.changedRoot);
			if (Debug.debugNotifyDeferred) {
				String p = this.changedRoot.getNodeName();
				System.out.println("Deferred STRUCTURE_CHANGED: " + p); //$NON-NLS-1$
			}
			this.changedRoot = null;
		}
		this.changing = false;
	}

	/**
	 */
	public void endTagChanged(Element element) {
		if (element == null)
			return;
		IDOMNode notifier = (IDOMNode) element;
		int offset = notifier.getStartOffset();
		notify(notifier, INodeNotifier.CHANGE, null, null, null, offset);
		propertyChanged(element);
	}

	/**
	 */
	public boolean hasChanged() {
		return (this.fEvents != null);
	}

	/**
	 */
	public boolean isChanging() {
		return this.changing;
	}

	/**
	 */
	private void notify(INodeNotifier notifier, int eventType, Object changedFeature, Object oldValue, Object newValue, int pos) {
		if (notifier == null)
			return;
		if (this.changing && !this.flushing) {
			// defer notification
			if (this.fEvents == null)
				this.fEvents = new ArrayList();
			// we do not defer anything if we are doing a new Model,
			// except for the document event, since all others are
			// trivial and not needed at that initial point.
			// But even for that one document event, in the new model case,
			// it is still important to defer it.
			if ((!doingNewModel) || (((Node) notifier).getNodeType() == Node.DOCUMENT_NODE)) {
				this.fEvents.add(new NotifyEvent(notifier, eventType, changedFeature, oldValue, newValue, pos));
			}
			return;
		}
		try {
			// Its important to "keep going" if exception occurs, since this
			// notification
			// comes in between "about to change" and "changed" events. We do
			// log, however,
			// since would indicate a program error.
			notifier.notify(eventType, changedFeature, oldValue, newValue, pos);
		} catch (Exception e) {
			Logger.logException("A structured model client threw following exception during adapter notification (" + INodeNotifier.EVENT_TYPE_STRINGS[eventType] + " )", e); //$NON-NLS-1$ //$NON-NLS-2$
		}
	}

	/**
	 */
	private void notifyDeferred() {
		if (this.fEvents == null)
			return;
		if (this.flushing)
			return;
		this.flushing = true; // force notification
		int count = this.fEvents.size();
		
		if (!doingNewModel && fOptimizeDeferred) {
			Map notifyEvents = new HashMap();
			for (int i = 0; i < count; i++) {
				NotifyEvent event = (NotifyEvent) this.fEvents.get(i);
				if (event == null)
					continue; // error
				event.index = i;
				if(event.type == INodeNotifier.REMOVE) {
					addToMap(event.oldValue, event, notifyEvents);
				}
				if(event.type == INodeNotifier.ADD) {
					addToMap(event.newValue, event, notifyEvents);
				}
			}
			Iterator it = notifyEvents.values().iterator();
			while (it.hasNext()) {
				NotifyEvent[] es = (NotifyEvent[]) it.next();
				for (int i = 0; i < es.length - 1; i++) {
					NotifyEvent event = es[i];
					if(es[i].discarded) continue;
					NotifyEvent next = es[i + 1];
					if(es[i].type == INodeNotifier.ADD && next.type == INodeNotifier.REMOVE) {
						// Added then removed later, discard both
						event.discarded = true;
						next.discarded = true;
						if (Debug.debugNotifyDeferred) {
							event.reason = event.reason + ADDED_THEN_REMOVED + "(see " + next.index + ")"; //$NON-NLS-1$ //$NON-NLS-2$
							next.reason = next.reason + ADDED_THEN_REMOVED + "(see " + event.index + ")"; //$NON-NLS-1$ //$NON-NLS-2$
						}
					}
				}
			}
			for (int i = 0; i < count; i++) {
				NotifyEvent event = (NotifyEvent) this.fEvents.get(i);
				if (event == null)
					continue; // error
				if(event.discarded) continue;
				if (event.notifier != null && fOptimizeDeferredAccordingToParentAdded) {
					if (event.type == INodeNotifier.ADD) {
						NotifyEvent[] es = (NotifyEvent[])notifyEvents.get(event.notifier);
						if(es != null) for (int p = 0; p < es.length && es[p].index < event.index; p++) {
							NotifyEvent prev = es[p];
							if (prev.type == INodeNotifier.REMOVE && prev.oldValue == event.notifier) {
								// parent is reparented, do not discard
								if (Debug.debugNotifyDeferred) {
									event.reason = event.reason + PARENT_IS_REPARENTED + "(see " + prev.index + ")"; //$NON-NLS-1$ //$NON-NLS-2$
								}
								break;
							} else if (prev.type == INodeNotifier.ADD && prev.newValue == event.notifier) {
								// parent has been added, discard this
								event.discarded = true;
								if (Debug.debugNotifyDeferred) {
									event.reason = event.reason + PARENT_IS_ADDED + "(see " + prev.index + ")"; //$NON-NLS-1$ //$NON-NLS-2$
								}
								break;
							}
						}							
					}						
				}
				if(event.discarded) continue;
				if (event.notifier != null && fOptimizeDeferredAccordingToParentRemoved) {
					if (event.type == INodeNotifier.REMOVE) {
						NotifyEvent[] es = (NotifyEvent[])notifyEvents.get(event.notifier);
						if(es != null) for (int n = 0; n < es.length; n++) {
							NotifyEvent next = es[n];
							if(next.index > event.index && next.type == INodeNotifier.REMOVE) {
								if (next.oldValue == event.notifier) {
									// parent will be removed, discard this
									event.discarded = true;
									if (Debug.debugNotifyDeferred) {
										event.reason = event.reason + PARENT_IS_REMOVED_TOO + "(see " + next.index + ")"; //$NON-NLS-1$ //$NON-NLS-2$
									}
									break;
								}
							}
						}
					}						
				}
				if(event.discarded) continue;
			}
		}
		for (int i = 0; i < count; i++) {
			NotifyEvent event = (NotifyEvent) this.fEvents.get(i);
			if (event == null)
				continue; // error
			if(event.discarded) continue;
			notify(event.notifier, event.type, event.changedFeature, event.oldValue, event.newValue, event.pos);
		}
		if (Debug.debugNotifyDeferred) {
			for (int l = 0; l < count; l++) {
				NotifyEvent event = (NotifyEvent) this.fEvents.get(l);
				Object o = null;
				String t = null;
				if (event.type == INodeNotifier.ADD) {
					o = event.newValue;
					t = " + "; //$NON-NLS-1$
				} else if (event.type == INodeNotifier.REMOVE) {
					o = event.oldValue;
					t = " - "; //$NON-NLS-1$
				}
				if (o instanceof Element) {
					String p = ((Node) event.notifier).getNodeName();
					String c = ((Node) o).getNodeName();
					String d = (event.discarded ? "! " : "  "); //$NON-NLS-1$ //$NON-NLS-2$
					System.out.println(d + p + t + c);
				}
			}
		}
		this.flushing = false;
		this.fEvents = null;
	}

	void addToMap(Object o, NotifyEvent event, Map map) {
		if(o == null) return;
		Object x = map.get(o);
		if(x == null) {
			map.put(o, new NotifyEvent[]{event});
		} else {
			NotifyEvent[] es = (NotifyEvent[])x;
			NotifyEvent[] es2 = new NotifyEvent[es.length + 1];
			System.arraycopy(es, 0, es2, 0, es.length);
			es2[es.length] = event;
			map.put(o, es2);
		}
	}

	/**
	 */
	private void notifyStructureChanged(Node root) {
		if (root == null)
			return;
		INodeNotifier notifier = (INodeNotifier) root;
		try {
			// Its important to "keep going" if exception occurs, since this
			// notification
			// comes in between "about to change" and "changed" events. We do
			// log, however,
			// since would indicate a program error.
			notifier.notify(INodeNotifier.STRUCTURE_CHANGED, null, null, null, -1);
		} catch (Exception e) {
			Logger.logException("A structured model client threw following exception during adapter notification (" + INodeNotifier.EVENT_TYPE_STRINGS[INodeNotifier.STRUCTURE_CHANGED] + " )", e); //$NON-NLS-1$ //$NON-NLS-2$
		}

	}

	/**
	 */
	public void propertyChanged(Node node) {
	}

	/**
	 * @param node
	 */
	private void setCommonRootIfNeeded(Node node) {
		// defer notification
		if (this.changedRoot == null) {
			this.changedRoot = node;
		} else {
			// tiny optimization: if previous commonAncestor (changedRoot) is
			// already 'document',
			// or if already equal to this 'node',
			// then no need to re-calculate
			if (changedRoot.getNodeType() != Node.DOCUMENT_NODE && changedRoot != node) {
				Node common = ((NodeImpl) this.changedRoot).getCommonAncestor(node);
				if (common != null)
					this.changedRoot = common;
				else
					this.changedRoot = node;
			}
		}
	}

	/**
	 */
	public void startTagChanged(Element element) {
		if (element == null)
			return;
		IDOMNode notifier = (IDOMNode) element;
		int offset = notifier.getStartOffset();
		notify(notifier, INodeNotifier.CHANGE, null, null, null, offset);
		propertyChanged(element);
	}

	/**
	 */
	public void structureChanged(Node node) {
		if (node == null)
			return;
		if (isChanging()) {
			setCommonRootIfNeeded(node);
			if (Debug.debugNotifyDeferred) {
				String p = this.changedRoot.getNodeName();
				System.out.println("requested STRUCTURE_CHANGED: " + p); //$NON-NLS-1$
			}
			return;
		}
		if (Debug.debugNotifyDeferred) {
			String p = node.getNodeName();
			System.out.println("STRUCTURE_CHANGED: " + p); //$NON-NLS-1$
		}
		notifyStructureChanged(node);
	}

	/**
	 * valueChanged method
	 * 
	 * @param node
	 *            org.w3c.dom.Node
	 */
	public void valueChanged(Node node) {
		if (node == null)
			return;
		IDOMNode notifier = null;
		if (node.getNodeType() == Node.ATTRIBUTE_NODE) {
			Attr attr = (Attr) node;
			notifier = (IDOMNode) attr.getOwnerElement();
			// TODO_dmw: experimental: changed 06/29/2004 to send "structuure
			// changed" even for attribute value changes
			// there are pros and cons to considering attribute value
			// "structure changed". Will (re)consider
			// setCommonRootIfNeeded(notifier);
			if (notifier == null)
				return;
			String value = attr.getValue();
			int offset = notifier.getStartOffset();
			notify(notifier, INodeNotifier.CHANGE, attr, null, value, offset);
		} else {
			// note: we do not send structured changed event for content
			// changed
			notifier = (IDOMNode) node;
			String value = node.getNodeValue();
			int offset = notifier.getStartOffset();
			notify(notifier, INodeNotifier.CHANGE, null, null, value, offset);
			if (node.getNodeType() != Node.ELEMENT_NODE) {
				IDOMNode parent = (IDOMNode) node.getParentNode();
				if (parent != null) {
					notify(parent, INodeNotifier.CONTENT_CHANGED, node, null, value, offset);
				}
			}
		}
		propertyChanged(notifier);
	}
}
