/*******************************************************************************
 * Copyright (c) 2001, 2015 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.sse.ui.internal.reconcile;

import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.reconciler.DirtyRegion;
import org.eclipse.wst.sse.core.StructuredModelManager;
import org.eclipse.wst.sse.core.internal.model.ModelLifecycleEvent;
import org.eclipse.wst.sse.core.internal.provisional.IModelLifecycleListener;
import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel;
import org.eclipse.wst.sse.core.internal.provisional.IndexedRegion;
import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocument;

/**
 * An IStructuredModel aware Region Processor. Adds ModelLifecycle listener.
 * ModelLifecycle listener notifies us that some reinitialization needs to
 * take place.
 * 
 * Model aware "process()" Implements a IndexedRegion-based "contains()"
 * method using IStructuredModel.
 * 
 */
public class StructuredRegionProcessor extends DocumentRegionProcessor {
	class ModelLifecycleListener implements IModelLifecycleListener {
		IStructuredModel changing = null;
		/**
		 * @see org.eclipse.wst.sse.core.internal.provisional.IModelLifecycleListener#processPostModelEvent(org.eclipse.wst.sse.core.internal.model.ModelLifecycleEvent)
		 */
		public void processPostModelEvent(ModelLifecycleEvent event) {

			// if underlying StructuredDocument changed, need to reconnect it
			// here...
			// ex. file is modified outside the workbench
			if (event.getType() == ModelLifecycleEvent.MODEL_DOCUMENT_CHANGED) {
				if (changing != null && event.getModel() == changing) {
					IStructuredDocument sDoc = event.getModel().getStructuredDocument();

					if (DEBUG) {
						System.out.println("======================================================"); //$NON-NLS-1$
						System.out.println("StructuredRegionProcessor: DOCUMENT MODEL CHANGED TO: "); //$NON-NLS-1$
						System.out.println(sDoc.get());
						System.out.println("======================================================"); //$NON-NLS-1$
					}
					setDocument(sDoc);
				}
				changing = null;
			}
		}

		/**
		 * @see org.eclipse.wst.sse.core.internal.provisional.IModelLifecycleListener#processPreModelEvent(org.eclipse.wst.sse.core.internal.model.ModelLifecycleEvent)
		 */
		public void processPreModelEvent(ModelLifecycleEvent event) {
			if(fCurrentDoc != null && fCurrentModel != null) {
				if (event.getType() == ModelLifecycleEvent.MODEL_DOCUMENT_CHANGED && event.getModel() == fCurrentModel) {
					changing = event.getModel();
					flushDirtyRegionQueue();
					/*
					 * note: old annotations are removed via the strategies on
					 * AbstractStructuredTextReconcilingStrategy
					 * #setDocument(...)
					 */
				}
			}
		}
	}

	/**
	 * The life cycle listener used to listen to the current documents model even though
	 * this class does not hold onto a reference to the model.
	 */
	private IModelLifecycleListener fLifeCycleListener = new ModelLifecycleListener();
	/** Used to get the current model on demand so a model does not need to be held by permanently */
	private IDocument fCurrentDoc = null;
	
	/**
	 * Only to be used for content type and IModelLifecycleListener
	 * registration. All other uses should "get" the model --getting the model
	 * during these specific options can cause lock interruption by
	 * org.eclipse.core.internal.jobs.DeadlockDetector.
	 */
	private IStructuredModel fCurrentModel = null;

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.eclipse.wst.sse.ui.internal.reconcile.DirtyRegionProcessor#getOuterRegion(org.eclipse.jface.text.reconciler.DirtyRegion,
	 *      org.eclipse.jface.text.reconciler.DirtyRegion)
	 */
	protected DirtyRegion getOuterRegion(DirtyRegion root, DirtyRegion possible) {
		// first try simple region check if one region contains the other
		DirtyRegion outer = super.getOuterRegion(root, possible);
		if (outer == null && fCurrentDoc != null) {
			IStructuredModel sModel = null;
			try {
				sModel = getStructuredModelForRead(fCurrentDoc);
				if (sModel != null) {
					// now compare nodes
					IndexedRegion rootRegion = sModel.getIndexedRegion(root.getOffset());
					IndexedRegion possRegion = sModel.getIndexedRegion(possible.getOffset());
					if (rootRegion != null && possRegion != null) {
						int rootStart = rootRegion.getStartOffset();
						int possStart = possRegion.getStartOffset();
						// first just check if rootregion starts before
						// possregion
						if (rootStart <= possStart) {
							// check if possregion is inside rootregion
							outer = _getOuterRegion(root, possible, sModel, rootStart, possStart);
						}
						else {
							// otherwise if rootregion is inside possregion
							outer = _getOuterRegion(possible, root, sModel, possStart, rootStart);
						}
					}
				}
				
			} finally {
				if(sModel != null) {
					sModel.releaseFromRead();
				}
			}
		}
		return outer;
	}

	/**
	 * Assumes that when this method is called, region1's node start offset >=
	 * region2's node start offset. Determines if region1 contains region2 or
	 * vice versa. Returns region1 if:
	 * <ul>
	 * <li>region1's node region == region2's node region</li>
	 * <li>region1's node region contains region2's node region</li>
	 * </ul>
	 * Returns region2 if:
	 * <ul>
	 * <li>region1's node region and region2's node region starts at same
	 * offset but region2's node region is longer</li>
	 * </ul>
	 * Returns null otherwise.
	 * 
	 * @param region1
	 * @param region2
	 * @param sModel
	 * @param region1NodeStart
	 * @param region2NodeStart
	 * @return outer dirty region or null if none exists.
	 */
	private DirtyRegion _getOuterRegion(DirtyRegion region1, DirtyRegion region2, IStructuredModel sModel, int region1NodeStart, int region2NodeStart) {
		DirtyRegion outer = null;
		int region1NodeEnd = -1;
		int region2NodeEnd = -1;
		// then check if region1's end appears after
		// region2's end
		IndexedRegion region1EndNode = sModel.getIndexedRegion(region1.getOffset() + region1.getLength());
		if (region1EndNode == null) {
			// if no end, just assume region spans all the
			// way to the end so it includes other region
			outer = region1;
		}
		else {
			region1NodeEnd = region1EndNode.getEndOffset();
			IndexedRegion region2EndNode = sModel.getIndexedRegion(region2.getOffset() + region2.getLength());
			region2NodeEnd = region2EndNode != null ? region2EndNode.getEndOffset() : getDocument().getLength();
			if (region1NodeEnd >= region2NodeEnd) {
				// root contains or is equal to possible
				outer = region1;
			}
			else if (region1NodeStart == region2NodeStart && region2NodeEnd >= region1NodeEnd) {
				// possible contains root because they
				// both start at same place but possible
				// is longer
				outer = region2;
			}
		}
		if (DEBUG) {
			if (outer != null)
				System.out.println("checking if [" + region1NodeStart + ":" + region1NodeEnd + "] contains [" + region2NodeStart + ":" + region2NodeEnd + "] ... " + outer.toString()); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ //$NON-NLS-5$
			else
				System.out.println("checking if [" + region1NodeStart + ":" + region1NodeEnd + "] contains [" + region2NodeStart + ":" + region2NodeEnd + "] ... NO CONTAIN"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ //$NON-NLS-5$
		}
		return outer;
	}

	/**
	 * We already know content type from when we created the model, so just
	 * use that.
	 */
	protected String getContentType(IDocument doc) {
		if (fCurrentModel != null && doc == fCurrentModel.getStructuredDocument()) {
			/*
			 * Avoid "get"ting a model if we can, it may require lock
			 * acquisition
			 */
			return fCurrentModel.getContentTypeIdentifier();
		}

		String contentTypeId = null;
		IStructuredModel sModel = null;
		try {
			sModel = getStructuredModelForRead(doc);
			if (sModel != null) {
				contentTypeId = sModel.getContentTypeIdentifier();
			}
		}
		finally {
			if (sModel != null)
				sModel.releaseFromRead();
		}
		return contentTypeId;
	}


	/**
	 * Remember to release model after use!!
	 * 
	 * @return
	 */
	private IStructuredModel getStructuredModelForRead(IDocument doc) {
		IStructuredModel sModel = null;
		if (doc != null && doc instanceof IStructuredDocument)
			sModel = StructuredModelManager.getModelManager().getModelForRead((IStructuredDocument) doc);
		return sModel;
	}

	protected void process(DirtyRegion dirtyRegion) {
		if (!isInstalled() || isInRewriteSession() || dirtyRegion == null || getDocument() == null)
			return;

		// unhook old lifecycle listener
		if(fCurrentDoc != null) {
			IStructuredModel sModel = null;
			try {
				sModel = getStructuredModelForRead(fCurrentDoc);
				
				// use structured model to determine area to process
				if (sModel != null) {
					int start = dirtyRegion.getOffset();
					int end = start + dirtyRegion.getLength();
					IndexedRegion irStart = sModel.getIndexedRegion(start);
					IndexedRegion irEnd = sModel.getIndexedRegion(end);

					if (irStart != null) {
						start = Math.min(start, irStart.getStartOffset());
					}
					if (irEnd != null) {
						end = Math.max(end, irEnd.getEndOffset());
					}
					super.process(createDirtyRegion(start, end - start, DirtyRegion.INSERT));
				} else {
					super.process(dirtyRegion);
				}
			} finally {
				if(sModel != null) {
					sModel.releaseFromRead();
				}
			}
		} else {
			super.process(dirtyRegion);
		}
	}

	/**
	 * Reinitializes listeners and sets new document onall strategies.
	 * 
	 * @see org.eclipse.jface.text.reconciler.AbstractReconciler#reconcilerDocumentChanged(IDocument)
	 */
	protected void reconcilerDocumentChanged(IDocument newDocument) {
		setDocument(newDocument);
	}

	public void setDocument(IDocument newDocument) {
		/*
		 * unhook old lifecycle listener; it is important not to re-get the
		 * model at this point as we may be shutting down
		 */
		if (fCurrentModel != null) {
			fCurrentModel.removeModelLifecycleListener(fLifeCycleListener);
			fCurrentModel.releaseFromRead();
			fCurrentModel = null;
		}

		// set the new document
		super.setDocument(newDocument);
		fCurrentDoc = newDocument;

		// add new lifecycle listener
		if (fCurrentDoc != null) {
			fCurrentModel = getStructuredModelForRead(fCurrentDoc);
			if (fCurrentModel != null) {
				fCurrentModel.addModelLifecycleListener(fLifeCycleListener);
			}
		}
	}
}
