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


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

import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.jobs.Job;
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.IStructuredModel;
import org.eclipse.wst.sse.ui.internal.SSEUIMessages;
import org.eclipse.wst.xml.core.internal.contentmodel.modelquery.CMDocumentManager;
import org.eclipse.wst.xml.core.internal.contentmodel.modelquery.ModelQuery;
import org.eclipse.wst.xml.core.internal.contentmodel.modelqueryimpl.CMDocumentLoader;
import org.eclipse.wst.xml.core.internal.contentmodel.modelqueryimpl.InferredGrammarBuildingCMDocumentLoader;
import org.eclipse.wst.xml.core.internal.modelquery.ModelQueryUtil;
import org.eclipse.wst.xml.core.internal.provisional.document.IDOMModel;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;

/**
 * This class is used to observe changes in the DOM and perform
 * occasional'scans' to deduce information. We use a delay timer mechanism to
 * ensure scans are made every couple of seconds to avoid performance
 * problems. Currently this class is used to keep track of referenced grammar
 * uri's within the document ensure that they are loaded by the
 * CMDocumentManager. We might want to generalize this class to perform other
 * suplimental information gathering that is suitable for 'time delayed'
 * computation (error hints etc.).
 */
// TODO: Where should this class go?
public class DOMObserver {


	// An abstract adapter that ensures that the children of a new Node are
	// also adapted
	//
	abstract class DocumentAdapter implements INodeAdapter {
		public DocumentAdapter() {
		}

		public void connect(Document document) {
			((INodeNotifier) document).addAdapter(this);
			adapt(document.getDocumentElement());
		}

		public void dicconnect(Document document) {
			((INodeNotifier) document).removeAdapter(this);
		}

		public void adapt(Element element) {
			if (element != null) {
				if (((INodeNotifier) element).getExistingAdapter(this) == null) {
					((INodeNotifier) element).addAdapter(this);

					for (Node child = element.getFirstChild(); child != null; child = child.getNextSibling()) {
						if (child.getNodeType() == Node.ELEMENT_NODE) {
							adapt((Element) child);
						}
					}
				}
			}
		}

		public boolean isAdapterForType(Object type) {
			return type == this;
		}

		abstract public void notifyChanged(INodeNotifier notifier, int eventType, Object feature, Object oldValue, Object newValue, int index);
	}

	/**
	 * This class listens to the changes in the CMDocument and triggers a
	 * CMDocument load
	 */
	class MyDocumentAdapter extends DocumentAdapter {

		public void notifyChanged(INodeNotifier notifier, int eventType, Object feature, Object oldValue, Object newValue, int index) {
			switch (eventType) {
				case INodeNotifier.ADD : {
					if (newValue instanceof Element) {
						// System.out.println("ADD (to " +
						// ((Node)notifier).getNodeName() + ") " +
						// ((Element)newValue).getNodeName() + " old " +
						// oldValue);
						adapt((Element) newValue);
					}
					break;
				}
					// case INodeNotifier.REMOVE:
				case INodeNotifier.CHANGE :
				case INodeNotifier.STRUCTURE_CHANGED :
				case INodeNotifier.CONTENT_CHANGED : {
					Node node = (Node) notifier;
					if (node.getNodeType() == Node.ELEMENT_NODE) {
						switch (eventType) {
							case INodeNotifier.CHANGE :
							case INodeNotifier.STRUCTURE_CHANGED : {
								// structure change
								invokeDelayedCMDocumentLoad();
								break;
							}
							case INodeNotifier.CONTENT_CHANGED : {
								// some content changed
								break;
							}
						}
					}
					else if (node.getNodeType() == Node.DOCUMENT_NODE) {
						invokeDelayedCMDocumentLoad();
					}
					break;
				}
			}
		}
	}

	/**
	 * Intentionally left visible to the user
	 */
	class TimerJob extends Job {
		public TimerJob() {
			super(SSEUIMessages.LoadingReferencedGrammars);
		}

		public IStatus run(IProgressMonitor monitor) {
			monitor.beginTask("", IProgressMonitor.UNKNOWN); //$NON-NLS-1$
			Thread.currentThread().setPriority(Thread.MIN_PRIORITY);
			invokeCMDocumentLoad();
			monitor.done();
			return Status.OK_STATUS;
		}
	}

	private Job timer = new TimerJob();
	protected Document fDocument;
	protected boolean isGrammarInferenceEnabled;
	/**
	 * If true, DOMObserver is currently disabled and not loading the content
	 * model
	 */
	private boolean fIsDisabled = false;
	/**
	 * If true, DOMObserver is currently trying to load the content model
	 */
	private boolean fIsLoading = false;

	public DOMObserver(IStructuredModel model) {
		fDocument = (model instanceof IDOMModel) ? ((IDOMModel) model).getDocument() : null;

		if (fDocument != null) {
			// here we create and init an adapter that will listen to
			// changes to the document and contained elements
			Collection adapters = ((INodeNotifier) fDocument).getAdapters();
			Iterator iterator = adapters.iterator();
			while (iterator.hasNext()) {
				INodeAdapter adapter = (INodeAdapter) iterator.next();
				if (adapter instanceof MyDocumentAdapter) {
					return;
				}
			}
			MyDocumentAdapter adapter = new MyDocumentAdapter();
			adapter.connect(fDocument);

			ModelQuery modelQuery = ModelQueryUtil.getModelQuery(fDocument);
			if ((modelQuery != null) && (modelQuery.getCMDocumentManager() != null)) {
				CMDocumentManager cmDocumentManager = modelQuery.getCMDocumentManager();
				cmDocumentManager.setPropertyEnabled(CMDocumentManager.PROPERTY_AUTO_LOAD, false);
			}
			
			// attach a dom observer adapter to the document so others have access to
			// domobserver if needed
			INodeAdapter domObserverAdapter = ((INodeNotifier)fDocument).getExistingAdapter(DOMObserverAdapter.class);
			if (domObserverAdapter == null) {
				domObserverAdapter = new DOMObserverAdapter();
				((INodeNotifier)fDocument).addAdapter(domObserverAdapter);
			}
			((DOMObserverAdapter)domObserverAdapter).setDOMObserver(this);
		}
	}

	public void init() {
		// CS: we seem to expose an XSD initialization problem when we do this
		// immediately
		// very nasty... I need to revist this problem with Ed Merks
		//
		timer.schedule();
	}

	public void invokeCMDocumentLoad() {
		if (fIsDisabled) return;
		try {
			fIsLoading = true;
			
			ModelQuery modelQuery = ModelQueryUtil.getModelQuery(fDocument);
			if ((modelQuery != null) && (modelQuery.getCMDocumentManager() != null)) {
				CMDocumentLoader loader = isGrammarInferenceEnabled ? new InferredGrammarBuildingCMDocumentLoader(fDocument, modelQuery) : new CMDocumentLoader(fDocument, modelQuery);
				loader.loadCMDocuments();
			}
		} finally {
			fIsLoading = false;
		}
	}

	public void invokeDelayedCMDocumentLoad() {
		if (fIsDisabled) return;
		timer.schedule(2000);
	}

	public void setGrammarInferenceEnabled(boolean isEnabled) {
		isGrammarInferenceEnabled = isEnabled;
	}
	
	boolean setDisabled(boolean isDisabled, boolean forced) {
		boolean success = true;
		
		if (fIsDisabled != isDisabled) {
			fIsDisabled = isDisabled;
			if (forced) {
				if (isDisabled)
					success = timer.cancel();
				else
					invokeCMDocumentLoad();
			}
		}
		return success;
	}
	
	boolean isLoading() {
		return fIsLoading;
	}
}
