/*******************************************************************************
 * Copyright (c) 2000, 2008 QNX Software Systems 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:
 *     QNX Software Systems - Initial API and implementation
 *     Anton Leherbauer (Wind River Systems)
 *******************************************************************************/
package org.eclipse.cdt.ui;



import java.util.HashSet;

import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IResourceDelta;
import org.eclipse.jface.text.IRegion;
import org.eclipse.jface.text.ITextViewer;
import org.eclipse.jface.text.Region;
import org.eclipse.jface.text.information.IInformationProvider;
import org.eclipse.jface.text.information.IInformationProviderExtension;
import org.eclipse.jface.viewers.StructuredViewer;
import org.eclipse.jface.viewers.Viewer;
import org.eclipse.swt.widgets.Control;
import org.eclipse.ui.texteditor.ITextEditor;

import org.eclipse.cdt.core.model.CModelException;
import org.eclipse.cdt.core.model.CoreModel;
import org.eclipse.cdt.core.model.ElementChangedEvent;
import org.eclipse.cdt.core.model.IArchive;
import org.eclipse.cdt.core.model.IBinary;
import org.eclipse.cdt.core.model.ICElement;
import org.eclipse.cdt.core.model.ICElementDelta;
import org.eclipse.cdt.core.model.ICModel;
import org.eclipse.cdt.core.model.ICProject;
import org.eclipse.cdt.core.model.IElementChangedListener;
import org.eclipse.cdt.core.model.IParent;
import org.eclipse.cdt.core.model.ITranslationUnit;
import org.eclipse.cdt.core.model.IWorkingCopy;

import org.eclipse.cdt.internal.core.model.ArchiveContainer;
import org.eclipse.cdt.internal.core.model.BinaryContainer;

import org.eclipse.cdt.internal.ui.BaseCElementContentProvider;
import org.eclipse.cdt.internal.ui.actions.SelectionConverter;
import org.eclipse.cdt.internal.ui.text.CWordFinder;

/**
 * A content provider for C elements.
 * <p>
 * The following C element hierarchy is surfaced by this content provider:
 * <p>
 * <pre>
C model (<code>ICModel</code>)<br>
   C project (<code>ICProject</code>)<br>
      Virtual binaries  container(<code>IBinaryContainery</code>)
      Virtual archives  container(<code>IArchiveContainery</code>)
      Source root (<code>ISourceRoot</code>)<br>
          C Container(folders) (<code>ICContainer</code>)<br>
          Translation unit (<code>ITranslationUnit</code>)<br>
          Binary file (<code>IBinary</code>)<br>
          Archive file (<code>IArchive</code>)<br>
      Non C Resource file (<code>Object</code>)<br>

 * </pre>
 */
public class CElementContentProvider extends BaseCElementContentProvider implements IElementChangedListener, IInformationProvider, IInformationProviderExtension{

	/** Editor. */
    protected ITextEditor fEditor;
    protected StructuredViewer fViewer;
	protected Object fInput;
	
	/** Remember what refreshes we already have pending so we don't post them again. */
	protected HashSet<IRefreshable> pendingRefreshes = new HashSet<IRefreshable>();

    /**
     * Creates a new content provider for C elements.
     */
    public CElementContentProvider()
    {
        // Empty.
    }
    
	/**
	 * Creates a new content provider for C elements.
     * @param editor Editor.
	 */
	public CElementContentProvider(ITextEditor editor)
    {
        fEditor = editor;
	}
    
	/**
	 * Creates a new content provider for C elements.
	 */
	public CElementContentProvider(boolean provideMembers, boolean provideWorkingCopy) {
		super(provideMembers, provideWorkingCopy);
	}

    /**
     * @see org.eclipse.jface.viewers.IContentProvider#dispose()
     */
    @Override
	public void dispose() {
        super.dispose();
        CoreModel.getDefault().removeElementChangedListener(this);
    }

    /**
     * @see org.eclipse.jface.viewers.IContentProvider#inputChanged(org.eclipse.jface.viewers.Viewer, java.lang.Object, java.lang.Object)
     */
    @Override
	public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {

        super.inputChanged(viewer, oldInput, newInput);

        fViewer = (StructuredViewer) viewer;

        if (oldInput == null && newInput != null) {
            CoreModel.getDefault().addElementChangedListener(this);
        } else if (oldInput != null && newInput == null) {
            CoreModel.getDefault().removeElementChangedListener(this);
        }
        fInput= newInput;
    }

	/**
     * @see org.eclipse.cdt.core.model.IElementChangedListener#elementChanged(org.eclipse.cdt.core.model.ElementChangedEvent)
	 */
    public void elementChanged(final ElementChangedEvent event) {
		try {
			processDelta(event.getDelta());
		} catch(CModelException e) {
			CUIPlugin.log(e);
			e.printStackTrace();
		}
	}

	protected boolean isPathEntryChange(ICElementDelta delta) {
		int flags= delta.getFlags();
		return (delta.getKind() == ICElementDelta.CHANGED &&
				((flags & ICElementDelta.F_BINARY_PARSER_CHANGED) != 0 ||
				(flags & ICElementDelta.F_ADDED_PATHENTRY_LIBRARY) != 0 ||
				(flags & ICElementDelta.F_ADDED_PATHENTRY_SOURCE) != 0 ||
				(flags & ICElementDelta.F_REMOVED_PATHENTRY_LIBRARY) != 0 ||
				(flags & ICElementDelta.F_PATHENTRY_REORDER) != 0 ||
				(flags & ICElementDelta.F_REMOVED_PATHENTRY_SOURCE) != 0 ||
				(flags & ICElementDelta.F_CHANGED_PATHENTRY_INCLUDE) != 0));
	}

	/**
	 * Processes a delta recursively. When more than two children are affected the
	 * tree is fully refreshed starting at this node. The delta is processed in the
	 * current thread but the viewer updates are posted to the UI thread.
	 */
	protected void processDelta(ICElementDelta delta) throws CModelException {
		int kind= delta.getKind();
		int flags= delta.getFlags();
		ICElement element= delta.getElement();

		//System.out.println("Processing " + element);

		// handle open and closing of a project
		if (((flags & ICElementDelta.F_CLOSED) != 0) || ((flags & ICElementDelta.F_OPENED) != 0)) {
			postRefresh(element);
			return;
		}

		// We do not care about changes in Working copies
		// well, we do see bugzilla 147694
		if (element instanceof ITranslationUnit) {
			ITranslationUnit unit = (ITranslationUnit) element;
			if (unit.isWorkingCopy()) {
				if (!getProvideWorkingCopy() || kind == ICElementDelta.REMOVED || kind == ICElementDelta.ADDED) {
					return;
				}
			}
			if (!getProvideMembers() && kind == ICElementDelta.CHANGED) {
				return;
			}
		}

		if (kind == ICElementDelta.REMOVED) {
			postRemove(element);
			updateContainer(element);
			return;
		}

		if (kind == ICElementDelta.ADDED) {
			Object parent= internalGetParent(element);
			postAdd(parent, element);
			updateContainer(element);
		}

		if (kind == ICElementDelta.CHANGED) {
			// Binary/Archive changes is done differently since they
			// are at two places, they are in the {Binary,Archive}Container
			// and in the Tree hiearchy
			if (updateContainer(element)) {
				Object parent = getParent(element);
				postRefresh(parent);
				return;
			} else if (element instanceof ITranslationUnit) {
				postRefresh(element);
				return;
			} else if (element instanceof ArchiveContainer || element instanceof BinaryContainer) {
				postContainerRefresh((IParent) element, element.getCProject());
			}

		}

		if (isPathEntryChange(delta)) {
			 // throw the towel and do a full refresh of the affected C project.
			postRefresh(element.getCProject());
			return;// bailout
		}
		
		if (processResourceDeltas(delta.getResourceDeltas(), element))
			return;
	
		ICElementDelta[] affectedChildren= delta.getAffectedChildren();
		for (ICElementDelta element2 : affectedChildren) {
			processDelta(element2);
		}
	}

	/**
	 * Process resource deltas.
	 *
	 * @return true if the parent got refreshed
	 */
	private boolean processResourceDeltas(IResourceDelta[] deltas, Object parent) {
		if (deltas == null)
			return false;
		
		if (deltas.length > 1 && !(parent instanceof ICModel)) {
			// more than one child changed, refresh from here downwards
			// but not if the parent is ICModel
			// see https://bugs.eclipse.org/bugs/show_bug.cgi?id=202085
			postRefresh(parent);
			return true;
		}

		for (IResourceDelta delta : deltas) {
			if (processResourceDelta(delta, parent))
				return true;
		}

		return false;
	}

	/**
	 * Process a resource delta.
	 * 
	 * @return true if the parent got refreshed
	 */
	private boolean processResourceDelta(IResourceDelta delta, Object parent) {
		int status= delta.getKind();
		IResource resource= delta.getResource();
		// filter out changes affecting the output folder
		if (resource == null) {
			return false;
		}
                        
		// this could be optimized by handling all the added children in the parent
		if ((status & IResourceDelta.REMOVED) != 0) {
			postRemove(resource);
		}
		if ((status & IResourceDelta.ADDED) != 0) {
			postAdd(parent, resource);
		}

		int flags= delta.getFlags();
		// open/close state change of a project
		if ((flags & IResourceDelta.OPEN) != 0) {
			postProjectStateChanged(parent);
			return true;
		}

		processResourceDeltas(delta.getAffectedChildren(), resource);
		return false;
	}

	private boolean updateContainer(ICElement cfile) throws CModelException {
		IParent container = null;
		ICProject cproject = null;
		if (cfile instanceof IBinary) {
			IBinary bin = (IBinary)cfile;
			if (bin.showInBinaryContainer()) {
				cproject = bin.getCProject();
				container = cproject.getBinaryContainer();
			}
		} else if (cfile instanceof IArchive) {
			cproject = cfile.getCProject();
			container = cproject.getArchiveContainer();
		}
		if (container != null) {
			postContainerRefresh(container, cproject);
			return true;
		}
		return false;
	}
	
	// Tree refresh system
	// We keep track of what we're going to refresh and avoid posting multiple refresh
	// messages for the same elements. This avoids major performance issues where
	// we update tree views hundreds or thousands of times.
	protected interface IRefreshable {
	    public void refresh();
	}
	protected final class RefreshContainer implements IRefreshable {
		private IParent container;
		private Object project;
		public RefreshContainer(IParent container, Object project) {
			this.container = container;
			this.project = project;
		}
	    public void refresh() {
			if (container.hasChildren()) {
				if (fViewer.testFindItem(container) != null) {
					fViewer.refresh(container);
				} else {
					fViewer.refresh(project);
				}
			} else {
				fViewer.refresh(project);
			}
	    }
	    @Override
		public boolean equals(Object o) {
	    	if (o instanceof RefreshContainer) {
	    		RefreshContainer c = (RefreshContainer)o;
	    		return c.container.equals(container) && c.project.equals(project);
	    	}
	        return false;
	    }
	    @Override
		public int hashCode() {
	    	return container.hashCode()*10903143 + 31181;
	    }
	}
	protected final class RefreshElement implements IRefreshable {
		private Object element;
		public RefreshElement(Object element) {
			this.element = element;
		}
		public void refresh() {
			if (element instanceof IWorkingCopy){
				if (fViewer.testFindItem(element) != null){
					fViewer.refresh(element);
				} else {
					fViewer.refresh(((IWorkingCopy)element).getOriginalElement());
				}
			} else if (element instanceof IBinary) {
				fViewer.refresh(element, true);
			} else {
				fViewer.refresh(element);
			}
		}
	    @Override
		public boolean equals(Object o) {
	    	if (o instanceof RefreshElement) {
	    		RefreshElement c = (RefreshElement)o;
	    		return c.element.equals(element);
	    	}
	        return false;
	    }
	    @Override
		public int hashCode() {
	    	return element.hashCode()*7 + 490487;
	    }
	}

	protected final class RefreshProjectState implements IRefreshable {
		private Object element;
		public RefreshProjectState(Object element) {
			this.element = element;
		}
		public void refresh() {
			fViewer.refresh(element, true);
			// trigger a syntetic selection change so that action refresh their
			// enable state.
			fViewer.setSelection(fViewer.getSelection());
		}
	    @Override
		public boolean equals(Object o) {
	    	if (o instanceof RefreshElement) {
	    		RefreshElement c = (RefreshElement)o;
	    		return c.element.equals(element);
	    	}
	        return false;
	    }
	    @Override
		public int hashCode() {
	    	return element.hashCode()*11 + 490487;
	    }
	}

	protected void postContainerRefresh(final IParent container, final ICProject cproject) {
		//System.out.println("UI Container:" + cproject + " " + container);
		postRefreshable(new RefreshContainer(container, cproject));
	}

	protected void postRefresh(final Object element) {
		//System.out.println("UI refresh:" + element);
		postRefreshable(new RefreshElement(element));
	}

	protected void postAdd(final Object parent, final Object element) {
		//System.out.println("UI add:" + parent + " " + element);
		postRefreshable(new RefreshElement(parent));
	}

	protected void postRemove(final Object element) {
		//System.out.println("UI remove:" + element);
		postRefreshable(new RefreshElement(internalGetParent(element)));
	}

	protected void postProjectStateChanged(final Object root) {
		postRefreshable(new RefreshProjectState(root));
	}

	protected final void postRefreshable(final IRefreshable r) {
		Control ctrl= fViewer.getControl();
		if (ctrl != null && !ctrl.isDisposed()) {
			if (pendingRefreshes.contains(r))
				return;
			pendingRefreshes.add(r);
			ctrl.getDisplay().asyncExec(new Runnable() {
				public void run() {
					pendingRefreshes.remove(r);
					Control ctrl= fViewer.getControl();
					if (ctrl != null && !ctrl.isDisposed()) {
						r.refresh();
					}
				}
			});
		}
	}

    /*
     * @see org.eclipse.jface.text.information.IInformationProvider#getSubject(org.eclipse.jface.text.ITextViewer, int)
     */
    public IRegion getSubject(ITextViewer textViewer, int offset) {
		if (textViewer != null && fEditor != null) {
			IRegion region = CWordFinder.findWord(textViewer.getDocument(),
					offset);
			if (region != null) {
				return region;
			}
			return new Region(offset, 0);
		}
		return null;
	}

    /*
	 * @see org.eclipse.jface.text.information.IInformationProvider#getInformation(org.eclipse.jface.text.ITextViewer,
	 *      org.eclipse.jface.text.IRegion)
	 */
    public String getInformation(ITextViewer textViewer, IRegion subject) {
    	// deprecated API - not used anymore
        Object info = getInformation2(textViewer, subject);
        if (info != null) {
        	return info.toString();
        }
        return null;
    }

    /*
     * @see org.eclipse.jface.text.information.IInformationProviderExtension#getInformation2(org.eclipse.jface.text.ITextViewer, org.eclipse.jface.text.IRegion)
     */
    public Object getInformation2(ITextViewer textViewer, IRegion subject) {
		if (fEditor == null)
			return null;
		try {
			ICElement element = SelectionConverter.getElementAtOffset(fEditor);
			if (element != null) {
				return element;
			}
			return SelectionConverter.getInput(fEditor);
		} catch (CModelException e) {
			return null;
		}
	}
}
