/******************************************************************************* 
 * Copyright (c) 2000, 2003 IBM Corporation and others. 
 * All rights reserved. This program and the accompanying materials! 
 * are made available under the terms of the Common Public License v1.0 
 * which accompanies this distribution, and is available at 
 * http://www.eclipse.org/legal/cpl-v10.html 
 * 
 * Contributors: 
 *      IBM Corporation - initial API and implementation 
 * 		Cagatay Kavukcuoglu <cagatayk@acm.org> - Filter for markers in same project
************************************************************************/

package org.eclipse.ui.views.tasklist;

import java.util.Arrays;
import java.util.HashSet;

import org.eclipse.core.resources.IMarker;
import org.eclipse.core.resources.IMarkerDelta;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.runtime.IAdaptable;
import org.eclipse.core.runtime.IPath;
import org.eclipse.jface.viewers.Viewer;
import org.eclipse.jface.viewers.ViewerFilter;
import org.eclipse.ui.IContainmentAdapter;
import org.eclipse.ui.IMemento;
import org.eclipse.ui.IWorkingSet;
import org.eclipse.ui.IWorkingSetManager;
import org.eclipse.ui.PlatformUI;

class TasksFilter extends ViewerFilter implements Cloneable {

	public static final String[] ROOT_TYPES = new String[] { IMarker.PROBLEM, IMarker.TASK };

	// Filter on resource constants
	static final int ON_ANY_RESOURCE = 0;
	static final int ON_SELECTED_RESOURCE_ONLY = 1;
	static final int ON_SELECTED_RESOURCE_AND_CHILDREN = 2;
	static final int ON_ANY_RESOURCE_OF_SAME_PROJECT = 3; // added by cagatayk@acm.org
	static final int ON_WORKING_SET = 4;

	// Description filter kind constants
	static final int FILTER_CONTAINS = 0;
	static final int FILTER_DOES_NOT_CONTAIN = 1;

	//final static int MINIMUM_MARKER_LIMIT = 10;
	final static int DEFAULT_MARKER_LIMIT = 2000;
	//final static int MAXIMUM_MARKER_LIMIT = 20000;
		
	String[] types;
	int onResource;
	IWorkingSet workingSet;
	boolean filterOnDescription;
	int descriptionFilterKind;
	String descriptionFilter;
	boolean filterOnSeverity;
	int severityFilter;
	boolean filterOnPriority;
	int priorityFilter;
	boolean filterOnCompletion;
	int completionFilter;
	private boolean filterOnMarkerLimit = true;
	private int markerLimit = DEFAULT_MARKER_LIMIT;

	private static final String TAG_ID = "id"; //$NON-NLS-1$
	private static final String TAG_TYPE = "type";  //$NON-NLS-1$
	private static final String TAG_ON_RESOURCE = "onResource"; //$NON-NLS-1$
	private static final String TAG_WORKING_SET = "workingSet"; //$NON-NLS-1$	
	private static final String TAG_FILTER_ON_DESCRIPTION = "filterOnDescription"; //$NON-NLS-1$
	private static final String TAG_DESCRIPTION_FILTER_KIND = "descriptionFilterKind"; //$NON-NLS-1$
	private static final String TAG_DESCRIPTION_FILTER = "descriptionFilter"; //$NON-NLS-1$
	private static final String TAG_FILTER_ON_SEVERITY = "filterOnSeverity"; //$NON-NLS-1$
	private static final String TAG_SEVERITY_FILTER = "severityFilter"; //$NON-NLS-1$
	private static final String TAG_FILTER_ON_PRIORITY = "filterOnPriority"; //$NON-NLS-1$
	private static final String TAG_PRIORITY_FILTER = "priorityFilter"; //$NON-NLS-1$
	private static final String TAG_FILTER_ON_COMPLETION = "filterOnCompletion"; //$NON-NLS-1$
	private static final String TAG_COMPLETION_FILTER = "completionFilter"; //$NON-NLS-1$
	private static final String TAG_FILTER_ON_MARKER_LIMIT = "filterOnMarkerLimit"; //$NON-NLS-1$ 
	private static final String TAG_MARKER_LIMIT = "markerLimit"; //$NON-NLS-1$
	
	public TasksFilter() {
		reset();
	}
	
	boolean getFilterOnMarkerLimit() {
		return filterOnMarkerLimit;
	}
	
	void setFilterOnMarkerLimit(boolean filterOnMarkerLimit) {
		this.filterOnMarkerLimit = filterOnMarkerLimit;
	}
	
	int getMarkerLimit() {
		return markerLimit;
	}
	
	void setMarkerLimit(int markerLimit) {
		if (markerLimit < 1) {
			markerLimit = TasksFilter.DEFAULT_MARKER_LIMIT;
		}
	
		//if (markerLimit < TasksFilter.MINIMUM_MARKER_LIMIT) {
		//	markerLimit = TasksFilter.MINIMUM_MARKER_LIMIT;
		//} else if (markerLimit > TasksFilter.MAXIMUM_MARKER_LIMIT) {
		//	markerLimit = TasksFilter.MAXIMUM_MARKER_LIMIT;
		//} 
	
		this.markerLimit = markerLimit;
	}
	
	boolean checkDescription(String desc) {
		if (desc == null) {  // be paranoid
			desc = ""; //$NON-NLS-1$
		}
		boolean contains = containsSubstring(desc, descriptionFilter);
		return descriptionFilterKind == FILTER_CONTAINS ? contains : !contains;
	}
	
	public Object clone() {
		try {
			return super.clone();
		}
		catch (CloneNotSupportedException e) {
			throw new Error(); // shouldn't happen
		}
	}
	
	boolean containsSubstring(String string, String substring) {
		int strLen = string.length();
		int subLen = substring.length();
		int len = strLen - subLen;
		for (int i = 0; i <= len; ++i) {
			if (string.regionMatches(true, i, substring, 0, subLen)) {
				return true;
			}
		}
		return false;
	}

	/**
	 * Returns if the given resource is enclosed by a working set element.
	 * The IContainmentAdapter of each working set element is used for the
	 * containment test. If there is no IContainmentAdapter for a working 
	 * set element, a simple resource based test is used. 
	 * 
	 * @param element resource to test for enclosure by a working set
	 * 	element 
	 * @return true if element is enclosed by a working set element and 
	 * 	false otherwise. 
	 */
	private boolean isEnclosed(IResource element) {
		IPath elementPath = element.getFullPath();
		IAdaptable[] workingSetElements = workingSet.getElements();
		
		if (elementPath.isEmpty() || elementPath.isRoot()) {
			return false;
		}
		for (int i = 0; i < workingSetElements.length; i++) {
			IAdaptable workingSetElement = workingSetElements[i];
			IContainmentAdapter containmentAdapter = (IContainmentAdapter) workingSetElement.getAdapter(IContainmentAdapter.class);
			
			// if there is no IContainmentAdapter defined for the working  
			// set element type fall back to using resource based  
			// containment check 
			if (containmentAdapter != null) {
				if (containmentAdapter.contains(workingSetElement, element, IContainmentAdapter.CHECK_CONTEXT | IContainmentAdapter.CHECK_IF_CHILD | IContainmentAdapter.CHECK_IF_DESCENDANT))
					return true;
			} else if (isEnclosedResource(element, elementPath, workingSetElement)) {
				return true;
			}		
		}
		return false;
	}

	/**
	 * Returns if the given resource is enclosed by a working set element.
	 * A resource is enclosed if it is either a parent of a working set 
	 * element, a child of a working set element or a working set element
	 * itself.
	 * Simple path comparison is used. This is only guaranteed to return
	 * correct results for resource working set elements. 
	 * 
	 * @param element resource to test for enclosure by a working set
	 * 	element
	 * @param elementPath full, absolute path of the element to test 
	 * @return true if element is enclosed by a working set element and 
	 * 	false otherwise. 
	 */
	private boolean isEnclosedResource(IResource element, IPath elementPath, IAdaptable workingSetElement) {
		IResource workingSetResource = null;
		
		if (workingSetElement.equals(element))
			return true;
		if (workingSetElement instanceof IResource) {
			workingSetResource = (IResource) workingSetElement;
		}
		else {
			workingSetResource = (IResource) workingSetElement.getAdapter(IResource.class);
		}	
		if (workingSetResource != null) {
			IPath resourcePath = workingSetResource.getFullPath();
			if (resourcePath.isPrefixOf(elementPath))
				return true;
		}
		return false;
	}

	public void reset() {
		types = ROOT_TYPES;
		onResource = ON_ANY_RESOURCE;
		filterOnDescription = false;
		descriptionFilter = ""; //$NON-NLS-1$
		filterOnSeverity = false;
		severityFilter = 0;
		filterOnPriority = false;
		priorityFilter = 0;
		filterOnCompletion = false;
		completionFilter = 0;
		filterOnMarkerLimit = true;
		markerLimit = DEFAULT_MARKER_LIMIT;
	}

	/**
	 * @see IPersistable
	 */
	public void restoreState(IMemento memento) {
		IMemento children[] = memento.getChildren(TAG_TYPE);
		types = new String[children.length];
		for (int i = 0; i < children.length; i++){
			types[i] = children[i].getString(TAG_ID);
		}
		Integer ival = memento.getInteger(TAG_ON_RESOURCE);
		onResource = ival == null ? ON_ANY_RESOURCE : ival.intValue();
		restoreWorkingSet(memento.getString(TAG_WORKING_SET));
		ival = memento.getInteger(TAG_FILTER_ON_DESCRIPTION);
		filterOnDescription = ival != null && ival.intValue() == 1;
		ival = memento.getInteger(TAG_DESCRIPTION_FILTER_KIND);
		descriptionFilterKind = ival == null ? FILTER_CONTAINS : ival.intValue();
		descriptionFilter = memento.getString(TAG_DESCRIPTION_FILTER);
		if (descriptionFilter == null)
			descriptionFilter = ""; //$NON-NLS-1$
		ival = memento.getInteger(TAG_FILTER_ON_SEVERITY);
		filterOnSeverity = ival != null && ival.intValue() == 1;
		ival = memento.getInteger(TAG_SEVERITY_FILTER);
		severityFilter = ival == null ? 0 : ival.intValue();
		ival = memento.getInteger(TAG_FILTER_ON_PRIORITY);
		filterOnPriority = ival != null && ival.intValue() == 1;
		ival = memento.getInteger(TAG_PRIORITY_FILTER);
		priorityFilter = ival == null ? 0 : ival.intValue();
		ival = memento.getInteger(TAG_FILTER_ON_COMPLETION);
		filterOnCompletion = ival != null && ival.intValue() == 1;
		ival = memento.getInteger(TAG_COMPLETION_FILTER);
		completionFilter = ival == null ? 0 : ival.intValue();
		ival = memento.getInteger(TAG_FILTER_ON_MARKER_LIMIT);
		filterOnMarkerLimit = ival == null || ival.intValue() == 1;
		ival = memento.getInteger(TAG_MARKER_LIMIT);
		markerLimit = ival == null ? DEFAULT_MARKER_LIMIT : ival.intValue();
	}

	/**
	 * Restores the saved working set, if any.
	 * 
	 * @param the saved working set name or null
	 */
	private void restoreWorkingSet(String workingSetName) {
		if (workingSetName != null) {
			IWorkingSetManager workingSetManager = PlatformUI.getWorkbench().getWorkingSetManager();
			IWorkingSet workingSet = workingSetManager.getWorkingSet(workingSetName);
			
			if (workingSet != null) {
				this.workingSet = workingSet;
			}
		}
	}

	/**
	 * Saves the object state within a memento.
	 *
	 * @param memento a memento to receive the object state
	 */
	public void saveState(IMemento memento) {
		for (int i = 0; i < types.length; i++){
			memento.createChild(TAG_TYPE).putString(TAG_ID,types[i]);
		}
		memento.putInteger(TAG_ON_RESOURCE,onResource);
		if (workingSet != null) {
			memento.putString(TAG_WORKING_SET,workingSet.getName());
		}
		memento.putInteger(TAG_FILTER_ON_DESCRIPTION,filterOnDescription?1:0);
		memento.putInteger(TAG_DESCRIPTION_FILTER_KIND,descriptionFilterKind);
		memento.putString(TAG_DESCRIPTION_FILTER,descriptionFilter);
		memento.putInteger(TAG_FILTER_ON_SEVERITY,filterOnSeverity?1:0);
		memento.putInteger(TAG_SEVERITY_FILTER,severityFilter);
		memento.putInteger(TAG_FILTER_ON_PRIORITY,filterOnPriority?1:0);
		memento.putInteger(TAG_PRIORITY_FILTER,priorityFilter);
		memento.putInteger(TAG_FILTER_ON_COMPLETION,filterOnCompletion?1:0);
		memento.putInteger(TAG_COMPLETION_FILTER,completionFilter);
		memento.putInteger(TAG_FILTER_ON_MARKER_LIMIT,filterOnMarkerLimit?1:0);
		memento.putInteger(TAG_MARKER_LIMIT,markerLimit);
	}
	
	public boolean select(Viewer viewer, Object parentElement, Object element) {
		return select((IMarker) element);
	}
	
	public boolean select(IMarker marker) {
		// resource settings are handled by the content provider
		return selectByType(marker) && selectByAttributes(marker) && selectByWorkingSet(marker);
	}
	
	public boolean select(IMarkerDelta markerDelta) {
		// resource settings are handled by the content provider
		return selectByType(markerDelta) && selectByAttributes(markerDelta) && selectByWorkingSet(markerDelta);
	}
		
	private boolean selectByType(IMarker marker) {
		for (int i = 0; i < types.length; ++i) {
			if (MarkerUtil.isMarkerType(marker, types[i]))
				return true;
		}
		return false;
	}
	
	private boolean selectByType(IMarkerDelta markerDelta) {
		for (int i = 0; i < types.length; ++i) {
			if (markerDelta.isSubtypeOf(types[i]))
				return true;
		}
		return false;
	}
	
	/**
	 * Returns whether the specified marker should be filter out or not.
	 * 
	 * @param marker the marker to test
	 * @return 
	 * 	true=the marker should not be filtered out
	 * 	false=the marker should be filtered out
	 */
	private boolean selectByWorkingSet(IMarker marker) {
		if (workingSet == null || onResource != ON_WORKING_SET) {
			return true;
		}			
		IResource resource = (IResource) marker.getResource();
		if (resource != null) {
			return isEnclosed(resource);
		}
		return false;
	}

	/**
	 * Returns whether the specified marker delta should be filter out 
	 * or not.
	 * 
	 * @param markerDelta the marker delta to test
	 * @return 
	 * 	true=the marker delta should not be filtered out
	 * 	false=the marker delta should be filtered out
	 */
	private boolean selectByWorkingSet(IMarkerDelta markerDelta) {
		if (workingSet == null || onResource != ON_WORKING_SET) {
			return true;
		}			
		IResource resource = (IResource) markerDelta.getResource();
		if (resource != null) {
			return isEnclosed(resource);
		}
		return false;
	}

	/* 
	 * WARNING: selectByAttributes(IMarker) and selectByAttributes(IMarkerDelta) must correspond.
	 */
	
	private boolean selectByAttributes(IMarker marker) {
	
		// severity filter applies only to problems
		if (filterOnSeverity && MarkerUtil.isMarkerType(marker, IMarker.PROBLEM)) {
			int sev = MarkerUtil.getSeverity(marker);
			if ((severityFilter & (1 << sev)) == 0)
				return false;
		}
	
		// priority and completion filters apply only to tasks
		// avoid doing type check more than once
		if ((filterOnPriority || filterOnCompletion) && MarkerUtil.isMarkerType(marker, IMarker.TASK)) {
			if (filterOnPriority) {
				int pri = MarkerUtil.getPriority(marker);
				if ((priorityFilter & (1 << pri)) == 0)
					return false;
			}
			if (filterOnCompletion) {
				boolean complete = MarkerUtil.isComplete(marker);
				if ((completionFilter & (complete ? 2 : 1)) == 0)
					return false;
			}
		}
	
		// description applies to all markers
		if (filterOnDescription) {
			String desc = MarkerUtil.getMessage(marker);
			if (!checkDescription(desc))
				return false;
		}
		return true;
	}
	
	private boolean selectByAttributes(IMarkerDelta markerDelta) {
	
		// severity filter applies only to problems
		if (filterOnSeverity && markerDelta.isSubtypeOf(IMarker.PROBLEM)) {
			int sev = markerDelta.getAttribute(IMarker.SEVERITY, IMarker.SEVERITY_WARNING);
			if ((severityFilter & (1 << sev)) == 0)
				return false;
		}
	
		// priority and completion filters apply only to tasks
		// avoid doing type check more than once
		if ((filterOnPriority || filterOnCompletion) && markerDelta.isSubtypeOf(IMarker.TASK)) {
			if (filterOnPriority) {
				int pri = markerDelta.getAttribute(IMarker.PRIORITY, IMarker.PRIORITY_NORMAL);
				if ((priorityFilter & (1 << pri)) == 0)
					return false;
			}
			if (filterOnCompletion) {
				boolean complete = markerDelta.getAttribute(IMarker.DONE, false);
				if ((completionFilter & (complete ? 2 : 1)) == 0)
					return false;
			}
		}
	
		// description applies to all markers
		if (filterOnDescription) {
			String desc = markerDelta.getAttribute(IMarker.MESSAGE, ""); //$NON-NLS-1$
			if (!checkDescription(desc))
				return false;
		}
		return true;
	}
	
	
	/**
	 * Returns whether the filter is including all markers.
	 *
	 * @return <code>true</code> if the filter includes all markers, <code>false</code> if not
	 */
	public boolean isShowingAll() {
		if (filterOnDescription || filterOnSeverity || filterOnPriority || filterOnCompletion) {
			return false;
		}
		if (onResource != ON_ANY_RESOURCE) {
			return false;
		}
		
		HashSet set = new HashSet(Arrays.asList(types));
		if (set.size() != ROOT_TYPES.length) {
			return false;
		}
		for (int i = 0; i < ROOT_TYPES.length; ++i) {
			if (!set.contains(ROOT_TYPES[i])) {
				return false;
			}
		}
		return true;
	}
}
