/*******************************************************************************
 * Copyright (C) 2014 Google Inc 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:
 *	   Marcus Eng (Google) - initial API and implementation
 *	   Sergey Prigogin (Google)
 *******************************************************************************/
package org.eclipse.ui.internal.monitoring;

import java.lang.management.ThreadInfo;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;

import org.eclipse.core.runtime.Assert;
import org.eclipse.ui.monitoring.StackSample;
import org.eclipse.ui.monitoring.UiFreezeEvent;

/**
 * Checks if the {@link UiFreezeEvent} matches any defined filters.
 * <p>
 * <strong>This class is not thread safe.</strong>
 * </p>
 */
public class FilterHandler {
	private static final String DOUBLE_BACKSLASH = "\\\\"; //$NON-NLS-1$

	/** Reusable object used to avoid object creation in filtering methods. */
	private final CompoundName compoundName = new CompoundName("", ""); //$NON-NLS-1$//$NON-NLS-2$

	/**
	 * Groups the class name and method name defined in the filter.
	 */
	private static class StackFrame implements Comparable<StackFrame> {
		final String className;
		final String methodName;

		public StackFrame(String className, String methodName) {
			this.className = className;
			this.methodName = methodName;
		}

		@Override
		public int compareTo(StackFrame other) {
			int c = methodName.compareTo(other.methodName);
			if (c != 0) {
				return c;
			}
			return className.compareTo(other.className);
		}
	}

	private static class CompoundName implements CharSequence {
		private String first;
		private String last;

		CompoundName(String first, String last) {
			Assert.isNotNull(first);
			Assert.isNotNull(last);
			this.first = first;
			this.last = last;
		}

		void reset(String first, String last) {
			Assert.isNotNull(first);
			Assert.isNotNull(last);
			this.first = first;
			this.last = last;
		}

		@Override
		public int length() {
			return first.length() + 1 + last.length();
		}

		@Override
		public char charAt(int index) {
			int firstLen = first.length();
			if (index < firstLen) {
				return first.charAt(index);
			} else if (index == firstLen) {
				return '.';
			} else {
				return last.charAt(index - firstLen - 1);
			}
		}

		@Override
		public CharSequence subSequence(int start, int end) {
			int lastOffset = first.length() + 1; // Offset of the last name in the sequence.
			if (end < lastOffset) {
				return first.subSequence(start, end);
			} else if (start < lastOffset) {
				return new CompoundName(first.substring(start), last.substring(0, end - lastOffset));
			} else {
				return last.subSequence(start - lastOffset, end - lastOffset);
			}
		}

		@Override
		public String toString() {
			return first + '.' + last;
		}
	}

	private final StackFrame[] filterFrames;
	private final Pattern[] filterPatterns;

	/**
	 * Creates the filter.
	 *
	 * @param commaSeparatedMethods comma separated fully qualified method names to filter on.
	 *     Method names may contain wildcard characters '*' and '?'.
	 */
	public FilterHandler(String commaSeparatedMethods) {
		String[] filters = commaSeparatedMethods.split(","); //$NON-NLS-1$

		List<StackFrame> stackFrames = new ArrayList<>(filters.length);
		List<Pattern> stackPatterns = new ArrayList<>(filters.length);
		for (String filter : filters) {
			if (containsWildcards(filter)) {
				Pattern pattern = createPattern(filter);
				stackPatterns.add(pattern);
			} else {
				int lastDot = filter.lastIndexOf('.');
				stackFrames.add(lastDot >= 0 ?
						new StackFrame(filter.substring(0, lastDot), filter.substring(lastDot + 1)) :
						new StackFrame("", filter)); //$NON-NLS-1$
			}
		}

		stackFrames.sort(null);
		filterFrames = stackFrames.toArray(new StackFrame[stackFrames.size()]);
		filterPatterns = stackPatterns.toArray(new Pattern[stackPatterns.size()]);
	}

	/**
	 * Returns {@code true} if the stack samples do not contain filtered stack frames in the stack
	 * traces of the display thread.
	 *
	 * @param stackSamples the array containing stack trace samples for a long event in the first
	 *     {@code numSamples} elements
	 * @param numSamples the number of valid stack trace samples in the {@code stackSamples} array
	 * @param displayThreadId the ID of the display thread
	 */
	public boolean shouldLogEvent(StackSample[] stackSamples, int numSamples,
			long displayThreadId) {
		if (filterFrames.length != 0 || filterPatterns.length != 0) {
			for (int i = 0; i < numSamples; i++) {
				if (hasFilteredTraces(stackSamples[i].getStackTraces(), displayThreadId)) {
					return false;
				}
			}
		}
		return true;
	}

	/**
	 * Checks if the stack trace of the display thread contains any frame that matches the filter.
	 */
	private boolean hasFilteredTraces(ThreadInfo[] stackTraces, long displayThreadId) {
		for (ThreadInfo threadInfo : stackTraces) {
			if (threadInfo.getThreadId() == displayThreadId) {
				for (StackTraceElement element : threadInfo.getStackTrace()) {
					if (matchesFilter(element)) {
						return true;
					}
				}
				return false;
			}
		}

		MonitoringPlugin.logError(Messages.FilterHandler_missing_thread_error, null);
		return false;
	}

	/**
	 * Checks whether the given stack frame matches the filter.
	 */
	boolean matchesFilter(StackTraceElement stackFrame) {
		String className = stackFrame.getClassName();
		String methodName = stackFrame.getMethodName();
		if (filterPatterns.length != 0) {
			// Match against patterns in filterPatterns.
			compoundName.reset(className, methodName);
			for (Pattern pattern : filterPatterns) {
				if (pattern.matcher(compoundName).matches()) {
					return true;
				}
			}
		}
		// Binary search in filterFrames.
		int low = 0;
		int high = filterFrames.length;
		while (low < high) {
			int mid = (low + high) >>> 1;
			StackFrame filter = filterFrames[mid];
			int c = methodName.compareTo(filter.methodName);
			if (c == 0) {
				c = className.compareTo(filter.className);
			}
			if (c == 0) {
				return true;
			} else if (c < 0) {
				high = mid;
			} else {
				low = mid + 1;
			}
		}
		return false;
	}

	private boolean containsWildcards(String pattern) {
		for (int i = 0; i < pattern.length(); i++) {
			char c = pattern.charAt(i);
			if (c != '.' && !Character.isJavaIdentifierPart(c)) {
				return true;
			}
		}
		return false;
	}

	/**
	 * Converts a glob pattern to a regular expression.
	 *
	 * @param pattern The glob pattern to convert
	 * @return the equivalent regular expression pattern
	 * @throws PatternSyntaxException if compilation of the regular expression fails
	 */
	private static Pattern createPattern(String pattern) throws PatternSyntaxException {
		int len = pattern.length();
		StringBuilder buf = new StringBuilder(len * 2);
		boolean isEscaped = false;
		for (int i = 0; i < len; i++) {
			char c = pattern.charAt(i);
			switch (c) {
			case '\\':
				// The backslash is an escape character.
				if (!isEscaped) {
					isEscaped = true;
				} else {
					buf.append(DOUBLE_BACKSLASH);
					isEscaped = false;
				}
				break;
			// Characters that have to be escaped in a regular expression.
			case '(':
			case ')':
			case '{':
			case '}':
			case '.':
			case '[':
			case ']':
			case '$':
			case '^':
			case '+':
			case '|':
				if (isEscaped) {
					buf.append(DOUBLE_BACKSLASH);
					isEscaped = false;
				}
				buf.append('\\');
				buf.append(c);
				break;
			case '?':
				if (!isEscaped) {
					buf.append('.');
				} else {
					buf.append('\\');
					buf.append(c);
					isEscaped = false;
				}
				break;
			case '*':
				if (!isEscaped) {
					buf.append(".*"); //$NON-NLS-1$
				} else {
					buf.append('\\');
					buf.append(c);
					isEscaped = false;
				}
				break;
			default:
				if (isEscaped) {
					buf.append(DOUBLE_BACKSLASH);
					isEscaped = false;
				}
				buf.append(c);
				break;
			}
		}
		if (isEscaped) {
			buf.append(DOUBLE_BACKSLASH);
			isEscaped= false;
		}
		return Pattern.compile(buf.toString());
	}
}
