/*******************************************************************************
 * Copyright (c) 2006, 2008 Wind River Systems, Inc. and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *     Anton Leherbauer (Wind River Systems) - initial API and implementation
 *     Markus Schorn (Wind River Systems)
 *******************************************************************************/
package org.eclipse.cdt.internal.ui.text;

import org.eclipse.core.resources.IProject;
import org.eclipse.core.runtime.Assert;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.jobs.ISchedulingRule;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.ITextViewer;
import org.eclipse.jface.text.reconciler.DirtyRegion;
import org.eclipse.jface.text.reconciler.MonoReconciler;
import org.eclipse.swt.events.ShellAdapter;
import org.eclipse.swt.events.ShellEvent;
import org.eclipse.swt.events.ShellListener;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.ui.IEditorInput;
import org.eclipse.ui.IPartListener2;
import org.eclipse.ui.IWorkbenchPartReference;
import org.eclipse.ui.IWorkbenchPartSite;
import org.eclipse.ui.IWorkbenchWindow;
import org.eclipse.ui.texteditor.ITextEditor;

import org.eclipse.cdt.core.CCorePlugin;
import org.eclipse.cdt.core.index.IIndexChangeEvent;
import org.eclipse.cdt.core.index.IIndexChangeListener;
import org.eclipse.cdt.core.index.IIndexerStateEvent;
import org.eclipse.cdt.core.index.IIndexerStateListener;
import org.eclipse.cdt.core.model.CoreModel;
import org.eclipse.cdt.core.model.ElementChangedEvent;
import org.eclipse.cdt.core.model.ICElement;
import org.eclipse.cdt.core.model.ICElementDelta;
import org.eclipse.cdt.core.model.ICProject;
import org.eclipse.cdt.core.model.IElementChangedListener;
import org.eclipse.cdt.core.model.ITranslationUnit;
import org.eclipse.cdt.core.model.IWorkingCopy;
import org.eclipse.cdt.ui.CUIPlugin;
import org.eclipse.cdt.ui.IWorkingCopyManager;

/**
 * A single strategy C reconciler.
 * 
 * @since 4.0
 */
public class CReconciler extends MonoReconciler {

	static class SingletonJob extends Job implements ISchedulingRule {
		private Runnable fCode;

		SingletonJob(String name, Runnable code) {
			super(name);
			fCode= code;
			setPriority(Job.SHORT);
			setRule(this);
			setUser(false);
			setSystem(true);
		}

		/*
		 * @see org.eclipse.core.runtime.jobs.Job#run(org.eclipse.core.runtime.IProgressMonitor)
		 */
		@Override
		protected IStatus run(IProgressMonitor monitor) {
			if (!monitor.isCanceled()) {
				fCode.run();
			}
			return Status.OK_STATUS;
		}

		/*
		 * @see org.eclipse.core.runtime.jobs.ISchedulingRule#contains(org.eclipse.core.runtime.jobs.ISchedulingRule)
		 */
		public boolean contains(ISchedulingRule rule) {
			return rule == this;
		}

		/*
		 * @see org.eclipse.core.runtime.jobs.ISchedulingRule#isConflicting(org.eclipse.core.runtime.jobs.ISchedulingRule)
		 */
		public boolean isConflicting(ISchedulingRule rule) {
			return rule == this;
		}
		
	}

	/**
	 * Internal part listener for activating the reconciler.
	 */
	private class PartListener implements IPartListener2 {
		/*
		 * @see org.eclipse.ui.IPartListener2#partActivated(org.eclipse.ui.IWorkbenchPartReference)
		 */
		public void partActivated(IWorkbenchPartReference partRef) {
		}
		/*
		 * @see org.eclipse.ui.IPartListener2#partBroughtToTop(org.eclipse.ui.IWorkbenchPartReference)
		 */
		public void partBroughtToTop(IWorkbenchPartReference partRef) {
		}
		/*
		 * @see org.eclipse.ui.IPartListener2#partClosed(org.eclipse.ui.IWorkbenchPartReference)
		 */
		public void partClosed(IWorkbenchPartReference partRef) {
		}
		/*
		 * @see org.eclipse.ui.IPartListener2#partDeactivated(org.eclipse.ui.IWorkbenchPartReference)
		 */
		public void partDeactivated(IWorkbenchPartReference partRef) {
		}
		/*
		 * @see org.eclipse.ui.IPartListener2#partHidden(org.eclipse.ui.IWorkbenchPartReference)
		 */
		public void partHidden(IWorkbenchPartReference partRef) {
			if (partRef.getPart(false) == fTextEditor) {
				setEditorActive(false);
			}
		}
		/*
		 * @see org.eclipse.ui.IPartListener2#partInputChanged(org.eclipse.ui.IWorkbenchPartReference)
		 */
		public void partInputChanged(IWorkbenchPartReference partRef) {
		}
		/*
		 * @see org.eclipse.ui.IPartListener2#partOpened(org.eclipse.ui.IWorkbenchPartReference)
		 */
		public void partOpened(IWorkbenchPartReference partRef) {
		}
		/*
		 * @see org.eclipse.ui.IPartListener2#partVisible(org.eclipse.ui.IWorkbenchPartReference)
		 */
		public void partVisible(IWorkbenchPartReference partRef) {
			if (partRef.getPart(false) == fTextEditor) {
				if (hasCModelChanged())
					CReconciler.this.scheduleReconciling();
				setEditorActive(true);
			}
		}
	}

	/**
	 * Internal Shell activation listener for activating the reconciler.
	 */
	private class ActivationListener extends ShellAdapter {

		private Control fControl;

		public ActivationListener(Control control) {
			Assert.isNotNull(control);
			fControl= control;
		}

		/*
		 * @see org.eclipse.swt.events.ShellListener#shellActivated(org.eclipse.swt.events.ShellEvent)
		 */
		@Override
		public void shellActivated(ShellEvent e) {
			if (!fControl.isDisposed() && fControl.isVisible()) {
				if (hasCModelChanged())
					CReconciler.this.scheduleReconciling();
				setEditorActive(true);
			}
		}

		/*
		 * @see org.eclipse.swt.events.ShellListener#shellDeactivated(org.eclipse.swt.events.ShellEvent)
		 */
		@Override
		public void shellDeactivated(ShellEvent e) {
			if (!fControl.isDisposed() && fControl.getShell() == e.getSource()) {
				setEditorActive(false);
			}
		}
	}

	/**
	 * Internal C element changed listener
	 */
	private class ElementChangedListener implements IElementChangedListener {
		/*
		 * @see org.eclipse.cdt.core.model.IElementChangedListener#elementChanged(org.eclipse.cdt.core.model.ElementChangedEvent)
		 */
		public void elementChanged(ElementChangedEvent event) {
			if (event.getType() == ElementChangedEvent.POST_CHANGE) {
				if (isRelevantDelta(event.getDelta())) {
					if (!fIsReconciling && isEditorActive() && fInitialProcessDone) {
						CReconciler.this.scheduleReconciling();
					} else {
						setCModelChanged(true);
					}
				}
			}
		}

		private boolean isRelevantDelta(ICElementDelta delta) {
			final int flags = delta.getFlags();
			if ((flags & ICElementDelta.F_CONTENT) != 0) {
				if (!fIsReconciling && isRelevantElement(delta.getElement())) {
					// mark model changed, but don't update immediately
					fIndexerListener.ignoreChanges(false);
					setCModelChanged(true);
				} else if (delta.getElement() instanceof ITranslationUnit) {
					fIndexerListener.ignoreChanges(true);
				}
			}
			if ((flags & (
					ICElementDelta.F_CHANGED_PATHENTRY_INCLUDE | 
					ICElementDelta.F_CHANGED_PATHENTRY_MACRO
					)) != 0) {
				if (isRelevantProject(delta.getElement().getCProject())) {
					return true;
				}
			}
			if ((flags & ICElementDelta.F_CHILDREN) != 0) {
				ICElementDelta[] childDeltas= delta.getChangedChildren();
				for (int i = 0; i < childDeltas.length; i++) {
					if (isRelevantDelta(childDeltas[i])) {
						return true;
					}
				}
			}
			return false;
		}
	}

	private class IndexerListener implements IIndexerStateListener, IIndexChangeListener {
		private boolean fIndexChanged;
		private boolean fIgnoreChanges;

		/*
		 * @see org.eclipse.cdt.core.index.IIndexerStateListener#indexChanged(org.eclipse.cdt.core.index.IIndexerStateEvent)
		 */
		public void indexChanged(IIndexerStateEvent event) {
			if (event.indexerIsIdle()) {
				if (fIndexChanged || hasCModelChanged()) {
					fIndexChanged= false;
					if (!fIsReconciling && isEditorActive() && fInitialProcessDone) {
						CReconciler.this.scheduleReconciling();
					} else {
						setCModelChanged(true);
					}
				}
				fIgnoreChanges= false;
			}
		}

		public void ignoreChanges(boolean ignore) {
			fIgnoreChanges= ignore;
		}

		/*
		 * @see org.eclipse.cdt.core.index.IIndexChangeListener#indexChanged(org.eclipse.cdt.core.index.IIndexChangeEvent)
		 */
		public void indexChanged(IIndexChangeEvent event) {
			if (!fIndexChanged && !fIgnoreChanges && isRelevantProject(event.getAffectedProject())) {
				fIndexChanged= true;
			}
		}
		
	}

	/** The reconciler's editor */
	private ITextEditor fTextEditor;
	/** The part listener */
	private IPartListener2 fPartListener;
	/** The shell listener */
	private ShellListener fActivationListener;
	/** The C element changed listener.  */
	private IElementChangedListener fCElementChangedListener;
	/** The indexer listener */
	private IndexerListener fIndexerListener; 
	/** Tells whether the C model might have changed. */
	private volatile boolean fHasCModelChanged= false;
	/** Tells whether this reconciler's editor is active. */
	private volatile boolean fIsEditorActive= true;
	/** Tells whether a reconcile is in progress. */
	private volatile boolean fIsReconciling= false;
	
	private boolean fInitialProcessDone= false;
	private Job fTriggerReconcilerJob;

	/**
	 * Create a reconciler for the given editor and strategy.
	 * 
	 * @param editor the text editor
	 * @param strategy  the C reconciling strategy
	 */
	public CReconciler(ITextEditor editor, CCompositeReconcilingStrategy strategy) {
		super(strategy, false);
		fTextEditor= editor;
	}

	/*
	 * @see org.eclipse.jface.text.reconciler.IReconciler#install(org.eclipse.jface.text.ITextViewer)
	 */
	@Override
	public void install(ITextViewer textViewer) {
		super.install(textViewer);
		
		fPartListener= new PartListener();
		IWorkbenchPartSite site= fTextEditor.getSite();
		IWorkbenchWindow window= site.getWorkbenchWindow();
		window.getPartService().addPartListener(fPartListener);

		fActivationListener= new ActivationListener(textViewer.getTextWidget());
		Shell shell= window.getShell();
		shell.addShellListener(fActivationListener);

		fCElementChangedListener= new ElementChangedListener();
		CoreModel.getDefault().addElementChangedListener(fCElementChangedListener);
		
		fIndexerListener= new IndexerListener();
		CCorePlugin.getIndexManager().addIndexerStateListener(fIndexerListener);
		CCorePlugin.getIndexManager().addIndexChangeListener(fIndexerListener);
		
		fTriggerReconcilerJob= new SingletonJob("Trigger Reconciler", new Runnable() { //$NON-NLS-1$
			public void run() {
				forceReconciling();
			}});
	}

	/*
	 * @see org.eclipse.jface.text.reconciler.IReconciler#uninstall()
	 */
	@Override
	public void uninstall() {
		fTriggerReconcilerJob.cancel();
		
		IWorkbenchPartSite site= fTextEditor.getSite();
		IWorkbenchWindow window= site.getWorkbenchWindow();
		window.getPartService().removePartListener(fPartListener);
		fPartListener= null;

		Shell shell= window.getShell();
		if (shell != null && !shell.isDisposed())
			shell.removeShellListener(fActivationListener);
		fActivationListener= null;

		CoreModel.getDefault().removeElementChangedListener(fCElementChangedListener);
		fCElementChangedListener= null;

		CCorePlugin.getIndexManager().removeIndexerStateListener(fIndexerListener);
		CCorePlugin.getIndexManager().removeIndexChangeListener(fIndexerListener);
		fIndexerListener= null;
		super.uninstall();
	}

	protected void scheduleReconciling() {
		if (!fInitialProcessDone)
			return;
		if (fTriggerReconcilerJob.cancel()) {
			fTriggerReconcilerJob.schedule(50);
		}
	}

	/*
	 * @see org.eclipse.jface.text.reconciler.AbstractReconciler#forceReconciling()
	 */
	@Override
	protected void forceReconciling() {
		if (!fInitialProcessDone)
			return;
		super.forceReconciling();
	}

	/*
	 * @see org.eclipse.jface.text.reconciler.AbstractReconciler#aboutToBeReconciled()
	 */
	@Override
	protected void aboutToBeReconciled() {
		CCompositeReconcilingStrategy strategy= (CCompositeReconcilingStrategy)getReconcilingStrategy(IDocument.DEFAULT_CONTENT_TYPE);
		strategy.aboutToBeReconciled();
	}

	/*
	 * @see org.eclipse.jface.text.reconciler.MonoReconciler#initialProcess()
	 */
	@Override
	protected void initialProcess() {
		super.initialProcess();
		fInitialProcessDone= true;
		if (!fIsReconciling && isEditorActive() && hasCModelChanged()) {
			CReconciler.this.scheduleReconciling();
		} 
	}

	/*
	 * @see org.eclipse.jface.text.reconciler.MonoReconciler#process(org.eclipse.jface.text.reconciler.DirtyRegion)
	 */
	@Override
	protected void process(DirtyRegion dirtyRegion) {
		fIsReconciling= true;
		setCModelChanged(false);
		super.process(dirtyRegion);
		fIsReconciling= false;
	}

	/**
	 * Tells whether the C Model has changed or not.
	 *
	 * @return <code>true</code> iff the C Model has changed
	 */
	private synchronized boolean hasCModelChanged() {
		return fHasCModelChanged;
	}

	/**
	 * Sets whether the C Model has changed or not.
	 *
	 * @param state <code>true</code> iff the C model has changed
	 */
	private synchronized void setCModelChanged(boolean state) {
		fHasCModelChanged= state;
	}
	
	/**
	 * Tells whether this reconciler's editor is active.
	 *
	 * @return <code>true</code> iff the editor is active
	 */
	private synchronized boolean isEditorActive() {
		return fIsEditorActive;
	}

	/**
	 * Sets whether this reconciler's editor is active.
	 *
	 * @param state <code>true</code> iff the editor is active
	 */
	private synchronized void setEditorActive(boolean active) {
		fIsEditorActive= active;
		if (!active) {
			fTriggerReconcilerJob.cancel();
		}
	}

	public boolean isRelevantElement(ICElement element) {
		if (!fInitialProcessDone) {
			return false;
		}
		if (element instanceof IWorkingCopy) {
			return false;
		}
		if (element instanceof ITranslationUnit) {
			IEditorInput input= fTextEditor.getEditorInput();
			IWorkingCopyManager manager= CUIPlugin.getDefault().getWorkingCopyManager();				
			IWorkingCopy copy= manager.getWorkingCopy(input);
			if (copy == null || copy.getOriginalElement().equals(element)) {
				return false;
			}
			return isRelevantProject(copy.getCProject());
		}
		return false;
	}


	private boolean isRelevantProject(ICProject affectedProject) {
		if (affectedProject == null) {
			return false;
		}
		IEditorInput input= fTextEditor.getEditorInput();
		IWorkingCopyManager manager= CUIPlugin.getDefault().getWorkingCopyManager();				
		IWorkingCopy copy= manager.getWorkingCopy(input);
		if (copy == null) {
			return false;
		}
		if (copy.getCProject().equals(affectedProject)) {
			return true;
		}
		IProject project= copy.getCProject().getProject();
		if (project == null) {
			return false;
		}
		try {
			IProject[] referencedProjects= project.getReferencedProjects();
			for (int i= 0; i < referencedProjects.length; i++) {
				project= referencedProjects[i];
				if (project.equals(affectedProject.getProject())) {
					return true;
				}
			}
		} catch (CoreException exc) {
		}
		return false;
	}

}
