/*******************************************************************************
 *  Copyright (c) 2011, 2015 IBM Corporation and others.
 *
 *  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:
 *     Sascha Becher <s.becher@qualitype.de> - bug 360894
 *******************************************************************************/
package org.eclipse.pde.internal.ui.search;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import org.eclipse.jface.viewers.Viewer;
import org.eclipse.pde.core.plugin.IPluginAttribute;
import org.eclipse.pde.core.plugin.IPluginElement;
import org.eclipse.pde.core.plugin.IPluginExtension;
import org.eclipse.pde.core.plugin.IPluginObject;
import org.eclipse.pde.core.plugin.IPluginParent;
import org.eclipse.pde.internal.core.bundle.BundlePlugin;
import org.eclipse.pde.internal.ui.util.ExtensionsFilterUtil;
import org.eclipse.ui.dialogs.PatternFilter;

/**
 * An extended filtering capability for the filtered tree of ExtensionsPage. The
 * search criteria is splitted by / first. The resulting values are used to
 * perform a search on all node's values. All elements fitting at least one of
 * the split values will be displayed. This extensions does not compromise the
 * default filtering behaviour of the tree while providing the ability to
 * highlight related items such as commands along with their command images,
 * handlers, menu entries and activities.
 *
 * @see org.eclipse.ui.dialogs.FilteredTree
 * @since 3.8
 */
public class ExtensionsPatternFilter extends PatternFilter {

	/**
	 * Limits the maximum number of attributes handled by the filter
	 */
	public static final int ATTRIBUTE_LIMIT = 30;

	protected String fSearchPattern;

	protected Set<String> fSearchPatterns = new HashSet<>();
	protected final Set<IPluginObject> fMatchingLeafs = new HashSet<>();
	protected final Set<Object> fFoundAnyElementsCache = new HashSet<>();

	/**
	 * Check if the leaf element is a match with the filter text. The default behavior
	 * checks that the element name or extension point is a match employing wildcards.
	 * An implicit wild card is added at the end always (default behaviour).
	 *
	 * Subclasses should override this method.
	 *
	 * @param viewer
	 *            the viewer that contains the element
	 * @param element
	 *            the tree element to check
	 * @return true if the given element's label matches the filter text
	 */
	@Override
	protected boolean isLeafMatch(Viewer viewer, Object element) {
		// match element name or extension point with wildcards; modified default behaviour
		if (isNameMatch(element)) {
			return true;
		}
		// match all splitted attribute's values of IPluginElement against splitted filter patterns
		if (element instanceof IPluginElement) {
			return doIsLeafMatch((IPluginElement) element);
		}
		return false;
	}

	protected boolean doIsLeafMatch(IPluginElement pluginElement) {
		List<String> syntheticAttributes = ExtensionsFilterUtil.handlePropertyTester(pluginElement);
		if (fSearchPatterns != null && !fSearchPatterns.isEmpty()) {
			int attributeNumber = 0;
			for (String searchPattern : fSearchPatterns) {
				if (attributeNumber < fSearchPatterns.size() && attributeNumber < ATTRIBUTE_LIMIT) {
					boolean quoted = isQuoted(searchPattern);
					if (searchPattern != null && searchPattern.length() > 0) {
						if (quoted) {
							searchPattern = searchPattern.substring(1, searchPattern.length() - 1);
						}
						int attributeCount = pluginElement.getAttributeCount();
						IPluginAttribute[] elementAttributes = pluginElement.getAttributes();

						for (int i = 0; i < attributeCount; i++) {
							IPluginAttribute attributeElement = elementAttributes[i];
							if (attributeElement != null && attributeElement.getValue() != null) {
								String[] attributes = getAttributeSplit(attributeElement.getValue(), quoted);
								if (attributes != null) {
									List<String> attributeList = new ArrayList<>(Arrays.asList(attributes));
									attributeList.addAll(syntheticAttributes);
									if (matchWithAttributes(pluginElement, searchPattern, attributeElement.getName(), attributeList, quoted)) {
										return true;
									}
								}
							}
						}
						if (searchPattern.equalsIgnoreCase(pluginElement.getName())) {
							return true;
						}
					}
				}
				attributeNumber++;
			}
		}
		return false;
	}

	protected boolean isNameMatch(Object element) {
		if (element != null) {
			if (element instanceof IPluginElement) {
				if (super.wordMatches(((IPluginElement) element).getName())) {
					return true;
				}
			} else if (element instanceof IPluginExtension) {
				if (super.wordMatches(((IPluginExtension) element).getPoint())) {
					return true;
				}
			}
		}
		return false;
	}

	protected boolean matchWithAttributes(IPluginElement pluginElement, String searchPattern, String attributeName, List<String> attributeList, boolean quoted) {
		for (int k = 0; k < attributeList.size(); k++) {
			String attributeValue = attributeList.get(k);
			if (attributeValue != null && attributeValue.length() > 0) {
				if (!attributeValue.startsWith("%") || quoted) { //$NON-NLS-1$
					int delimiterPosition = attributeValue.indexOf('?'); // strip right of '?'
					if (delimiterPosition != -1) {
						attributeValue = attributeValue.substring(0, delimiterPosition);
					}
					// case insensitive exact match required
					if (attributeValue.equalsIgnoreCase(searchPattern)) {
						return true;
						// missing use of resource bundle localization requires wildcard enabled search
					} else if (!quoted && isNoneResourceMatch(attributeValue, attributeName, searchPattern)) {
						return true;
					}
				} else { // resource bundle key found
					String resourceValue = pluginElement.getResourceString(attributeValue);
					attributeValue = (resourceValue != null && resourceValue.length() > 0) ? resourceValue : attributeValue;
					super.setPattern(String.valueOf(searchPattern));
					// case insensitive match required with wildcards enabled
					boolean match = (super.wordMatches(attributeValue));
					super.setPattern(fSearchPattern);
					if (match) {
						return true;
					}
				}
			}
		}
		return false;
	}

	/**
	 * While the plugin model offers resource bundle localization, some plugins may skip this and use fix text for display.
	 * Wildcard enabled search of the PatternFilter should be available in this case. Only a list of attributes that
	 * are expected to contain resource bundles are evaluated as long as the value doesn't contain a point. On some elements
	 * for example a name attribute can contain an id. Those are skipped though.
	 *
	 * @return whether this is a match
	 */
	protected boolean isNoneResourceMatch(String attributeValue, String attributeName, String searchPattern) {
		if (ExtensionsFilterUtil.isAttributeNameMatch(attributeName, ExtensionsFilterUtil.RESOURCE_ATTRIBUTES)) {
			if (attributeValue.indexOf('.') == -1 && searchPattern.indexOf('.') == -1) { // no ids
				super.setPattern(searchPattern);
				boolean match = super.wordMatches(attributeValue);
				super.setPattern(fSearchPattern);
				if (match) {
					return true;
				}
				super.setPattern(fSearchPattern);
			}
		}
		return false;
	}

	static boolean isQuoted(String value) {
		return value.startsWith("\"") && value.endsWith("\""); //$NON-NLS-1$ //$NON-NLS-2$
	}

	/**
	 * Splits attributes on occurrence of /<br>
	 * If <code>quoted</code> is set to <code>true</code> parameter <code>text</code> is returned as the only
	 * element in the array, thus skipping the splitting.
	 *
	 * @param text text to split
	 * @param quoted decides whether splitting actually occurs
	 * @return split array containing the splitted attributes or one element containing the value of parameter <code>text</code>
	 */
	static String[] getAttributeSplit(String text, boolean quoted) {
		if (text.length() < 2) {
			return null;
		}
		if (!quoted) {
			return text.replaceAll("/{1,}", "/").split("/"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
		}
		return new String[] {text};
	}

	@Override
	public boolean isElementVisible(Viewer viewer, Object element) {
		if (fFoundAnyElementsCache.contains(element)) {
			return true;
		}
		return isLeafMatch(viewer, element);
	}

	@Override
	public Object[] filter(Viewer viewer, Object parent, Object[] elements) {
		if (parent != null && parent instanceof BundlePlugin) {
			if (fFoundAnyElementsCache.isEmpty() && fSearchPattern != null && fSearchPattern.length() > 0) {
				BundlePlugin pluginPlugin = (BundlePlugin) parent;
				doFilter(viewer, pluginPlugin, pluginPlugin.getExtensions(), false);
			}
		}
		if (!fFoundAnyElementsCache.isEmpty()) {
			List<Object> found = new ArrayList<>();
			for (Object element : elements) {
				if (fFoundAnyElementsCache.contains(element)) {
					found.add(element);
				}
			}
			return found.toArray();
		}
		return super.filter(viewer, parent, elements);
	}

	protected boolean doFilter(Viewer viewer, Object parent, IPluginObject[] children, boolean addChildren) {
		boolean isParentMatch = fFoundAnyElementsCache.contains(parent) ? true : false;

		// find leaf matches
		boolean isAnyLeafMatch = false;
		for (IPluginObject iPluginObject : children) {
			boolean isChildMatch = true;
			if (!isParentMatch || children.length > 0) {
				isChildMatch = this.isLeafMatch(viewer, iPluginObject);
				isAnyLeafMatch |= isChildMatch;
				if (isChildMatch) {
					fMatchingLeafs.add(iPluginObject);
				}
			}
			if (isChildMatch || addChildren) {
				fFoundAnyElementsCache.add(iPluginObject);
			}
		}

		// traverse children when available
		boolean isAnyChildMatch = false;
		for (IPluginObject iPluginObject : children) {
			if (iPluginObject instanceof IPluginParent pluginElement) {
				if (pluginElement.getChildren().length > 0) {
					boolean isChildrenMatch = doFilter(viewer, pluginElement, pluginElement.getChildren(), addChildren || fMatchingLeafs.contains(pluginElement));
					isAnyChildMatch |= isChildrenMatch;
					if (isChildrenMatch) {
						fFoundAnyElementsCache.add(pluginElement);
					}
				}
			}
		}
		return isAnyChildMatch || isAnyLeafMatch;
	}

	/**
	 * Splits a string at the occurrences of <code>/</code>. Any quoted parts of the <code>filterText</code>
	 * are not to be splitted but remain as a whole along with the quotation.
	 *
	 * @param filterText text to split
	 * @return split array
	 */
	protected String[] splitWithQuoting(String filterText) {
		// remove multiple separators
		String text = filterText.replaceAll("/{1,}", "/"); //$NON-NLS-1$//$NON-NLS-2$
		boolean containsQuoting = text.indexOf('\"') != -1;
		if (containsQuoting) {
			// remove multiple quotes
			text = text.replaceAll("\"{1,}", "\""); //$NON-NLS-1$//$NON-NLS-2$
			// treat quoted text as a whole, thus enables searching for file paths
			if (text.replaceAll("[^\"]", "").length() % 2 == 0) { //$NON-NLS-1$//$NON-NLS-2$
				return text.split("/(?=([^\"]*\"[^\"]*\")*(?![^\"]*\"))"); //$NON-NLS-1$
			} // filter text must have erroneous quoting, replacing all
			text = text.replaceAll("[\"]", ""); //$NON-NLS-1$ //$NON-NLS-2$
		}
		return text.split("/"); //$NON-NLS-1$
	}

	/**
	 * Enables the filter to temporarily display arbitrary elements
	 */
	public boolean addElement(Object element) {
		return fFoundAnyElementsCache.add(element);
	}

	/**
	 * Removes elements from the filter
	 */
	public boolean removeElement(Object element) {
		return fFoundAnyElementsCache.remove(element);
	}

	/*
	 * The pattern string for which this filter should select
	 * elements in the viewer.
	 *
	 * @see org.eclipse.ui.dialogs.PatternFilter#setPattern(java.lang.String)
	 */
	@Override
	public final void setPattern(String patternString) {
		super.setPattern(patternString);
		fSearchPattern = patternString;
		String[] patterns = (patternString != null) ? splitWithQuoting(patternString) : new String[] {};
		fSearchPatterns.clear();
		fSearchPatterns.addAll(Arrays.asList(patterns));
		fFoundAnyElementsCache.clear();
	}

	/**
	 * @return the whole filter text (unsplit)
	 */
	public String getPattern() {
		return fSearchPattern;
	}

	public void clearMatchingLeafs() {
		fMatchingLeafs.clear();
	}

	public Object[] getMatchingLeafsAsArray() {
		return fMatchingLeafs.toArray();
	}

	public Set<IPluginObject> getMatchingLeafs() {
		return fMatchingLeafs;
	}

	public boolean containsElement(Object element) {
		return fFoundAnyElementsCache.contains(element);
	}

}