/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.commons.lang3;

import java.time.Duration;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;

import org.apache.commons.lang3.time.DurationUtils;

/**
 * <p>
 * Helpers for {@code java.lang.Thread} and {@code java.lang.ThreadGroup}.
 * </p>
 * <p>
 * #ThreadSafe#
 * </p>
 *
 * @see java.lang.Thread
 * @see java.lang.ThreadGroup
 * @since 3.5
 */
public class ThreadUtils {

	/**
	 * A predicate implementation which always returns true.
	 */
	private static final class AlwaysTruePredicate implements ThreadPredicate, ThreadGroupPredicate {

		private AlwaysTruePredicate() {
		}

		@Override
		public boolean test(final Thread thread) {
			return true;
		}

		@Override
		public boolean test(final ThreadGroup threadGroup) {
			return true;
		}
	}

	/**
	 * A predicate implementation which matches a thread or threadgroup name.
	 */
	public static class NamePredicate implements ThreadPredicate, ThreadGroupPredicate {

		private final String name;

		/**
		 * Predicate constructor
		 *
		 * @param name thread or threadgroup name
		 * @throws IllegalArgumentException if the name is {@code null}
		 */
		public NamePredicate(final String name) {
			Validate.notNull(name, "name");
			this.name = name;
		}

		@Override
		public boolean test(final Thread thread) {
			return thread != null && thread.getName().equals(name);
		}

		@Override
		public boolean test(final ThreadGroup threadGroup) {
			return threadGroup != null && threadGroup.getName().equals(name);
		}
	}

	/**
	 * A predicate for selecting threadgroups.
	 */
	// When breaking BC, replace this with Predicate<ThreadGroup>
	@FunctionalInterface
	public interface ThreadGroupPredicate {

		/**
		 * Evaluates this predicate on the given threadgroup.
		 * 
		 * @param threadGroup the threadgroup
		 * @return {@code true} if the threadGroup matches the predicate, otherwise
		 *         {@code false}
		 */
		boolean test(ThreadGroup threadGroup);
	}

	/**
	 * A predicate implementation which matches a thread id.
	 */
	public static class ThreadIdPredicate implements ThreadPredicate {

		private final long threadId;

		/**
		 * Predicate constructor
		 *
		 * @param threadId the threadId to match
		 * @throws IllegalArgumentException if the threadId is zero or negative
		 */
		public ThreadIdPredicate(final long threadId) {
			if (threadId <= 0) {
				throw new IllegalArgumentException("The thread id must be greater than zero");
			}
			this.threadId = threadId;
		}

		@Override
		public boolean test(final Thread thread) {
			return thread != null && thread.getId() == threadId;
		}
	}

	/**
	 * A predicate for selecting threads.
	 */
	// When breaking BC, replace this with Predicate<Thread>
	@FunctionalInterface
	public interface ThreadPredicate {

		/**
		 * Evaluates this predicate on the given thread.
		 * 
		 * @param thread the thread
		 * @return {@code true} if the thread matches the predicate, otherwise
		 *         {@code false}
		 */
		boolean test(Thread thread);
	}

	/**
	 * Predicate which always returns true.
	 */
	public static final AlwaysTruePredicate ALWAYS_TRUE_PREDICATE = new AlwaysTruePredicate();

	/**
	 * Finds the active thread with the specified id.
	 *
	 * @param threadId The thread id
	 * @return The thread with the specified id or {@code null} if no such thread
	 *         exists
	 * @throws IllegalArgumentException if the specified id is zero or negative
	 * @throws SecurityException        if the current thread cannot access the
	 *                                  system thread group
	 *
	 * @throws SecurityException        if the current thread cannot modify thread
	 *                                  groups from this thread's thread group up to
	 *                                  the system thread group
	 */
	public static Thread findThreadById(final long threadId) {
		final Collection<Thread> result = findThreads(new ThreadIdPredicate(threadId));
		return result.isEmpty() ? null : result.iterator().next();
	}

	/**
	 * Finds the active thread with the specified id if it belongs to a thread group
	 * with the specified group name.
	 *
	 * @param threadId        The thread id
	 * @param threadGroupName The thread group name
	 * @return The threads which belongs to a thread group with the specified group
	 *         name and the thread's id match the specified id. {@code null} is
	 *         returned if no such thread exists
	 * @throws IllegalArgumentException if the specified id is zero or negative or
	 *                                  the group name is null
	 * @throws SecurityException        if the current thread cannot access the
	 *                                  system thread group
	 *
	 * @throws SecurityException        if the current thread cannot modify thread
	 *                                  groups from this thread's thread group up to
	 *                                  the system thread group
	 */
	public static Thread findThreadById(final long threadId, final String threadGroupName) {
		Validate.notNull(threadGroupName, "threadGroupName");
		final Thread thread = findThreadById(threadId);
		if (thread != null && thread.getThreadGroup() != null
				&& thread.getThreadGroup().getName().equals(threadGroupName)) {
			return thread;
		}
		return null;
	}

	/**
	 * Finds the active thread with the specified id if it belongs to the specified
	 * thread group.
	 *
	 * @param threadId    The thread id
	 * @param threadGroup The thread group
	 * @return The thread which belongs to a specified thread group and the thread's
	 *         id match the specified id. {@code null} is returned if no such thread
	 *         exists
	 * @throws IllegalArgumentException if the specified id is zero or negative or
	 *                                  the group is null
	 * @throws SecurityException        if the current thread cannot access the
	 *                                  system thread group
	 *
	 * @throws SecurityException        if the current thread cannot modify thread
	 *                                  groups from this thread's thread group up to
	 *                                  the system thread group
	 */
	public static Thread findThreadById(final long threadId, final ThreadGroup threadGroup) {
		Validate.notNull(threadGroup, "threadGroup");
		final Thread thread = findThreadById(threadId);
		if (thread != null && threadGroup.equals(thread.getThreadGroup())) {
			return thread;
		}
		return null;
	}

	/**
	 * Select all active threadgroups which match the given predicate and which is a
	 * subgroup of the given thread group (or one of its subgroups).
	 *
	 * @param group     the thread group
	 * @param recurse   if {@code true} then evaluate the predicate recursively on
	 *                  all threadgroups in all subgroups of the given group
	 * @param predicate the predicate
	 * @return An unmodifiable {@code Collection} of active threadgroups which match
	 *         the given predicate and which is a subgroup of the given thread group
	 * @throws IllegalArgumentException if the given group or predicate is null
	 * @throws SecurityException        if the current thread cannot modify thread
	 *                                  groups from this thread's thread group up to
	 *                                  the system thread group
	 */
	public static Collection<ThreadGroup> findThreadGroups(final ThreadGroup group, final boolean recurse,
			final ThreadGroupPredicate predicate) {
		Validate.notNull(group, "group");
		Validate.notNull(predicate, "predicate");

		int count = group.activeGroupCount();
		ThreadGroup[] threadGroups;
		do {
			threadGroups = new ThreadGroup[count + (count / 2) + 1]; // slightly grow the array size
			count = group.enumerate(threadGroups, recurse);
			// return value of enumerate() must be strictly less than the array size
			// according to javadoc
		} while (count >= threadGroups.length);

		final List<ThreadGroup> result = new ArrayList<>(count);
		for (int i = 0; i < count; ++i) {
			if (predicate.test(threadGroups[i])) {
				result.add(threadGroups[i]);
			}
		}
		return Collections.unmodifiableCollection(result);
	}

	/**
	 * Select all active threadgroups which match the given predicate.
	 *
	 * @param predicate the predicate
	 * @return An unmodifiable {@code Collection} of active threadgroups matching
	 *         the given predicate
	 * @throws IllegalArgumentException if the predicate is null
	 * @throws SecurityException        if the current thread cannot access the
	 *                                  system thread group
	 * @throws SecurityException        if the current thread cannot modify thread
	 *                                  groups from this thread's thread group up to
	 *                                  the system thread group
	 */
	public static Collection<ThreadGroup> findThreadGroups(final ThreadGroupPredicate predicate) {
		return findThreadGroups(getSystemThreadGroup(), true, predicate);
	}

	/**
	 * Finds active thread groups with the specified group name.
	 *
	 * @param threadGroupName The thread group name
	 * @return the thread groups with the specified group name or an empty
	 *         collection if no such thread group exists. The collection returned is
	 *         always unmodifiable.
	 * @throws IllegalArgumentException if group name is null
	 * @throws SecurityException        if the current thread cannot access the
	 *                                  system thread group
	 *
	 * @throws SecurityException        if the current thread cannot modify thread
	 *                                  groups from this thread's thread group up to
	 *                                  the system thread group
	 */
	public static Collection<ThreadGroup> findThreadGroupsByName(final String threadGroupName) {
		return findThreadGroups(new NamePredicate(threadGroupName));
	}

	/**
	 * Select all active threads which match the given predicate and which belongs
	 * to the given thread group (or one of its subgroups).
	 *
	 * @param group     the thread group
	 * @param recurse   if {@code true} then evaluate the predicate recursively on
	 *                  all threads in all subgroups of the given group
	 * @param predicate the predicate
	 * @return An unmodifiable {@code Collection} of active threads which match the
	 *         given predicate and which belongs to the given thread group
	 * @throws IllegalArgumentException if the given group or predicate is null
	 * @throws SecurityException        if the current thread cannot modify thread
	 *                                  groups from this thread's thread group up to
	 *                                  the system thread group
	 */
	public static Collection<Thread> findThreads(final ThreadGroup group, final boolean recurse,
			final ThreadPredicate predicate) {
		Validate.notNull(group, "The group must not be null");
		Validate.notNull(predicate, "The predicate must not be null");

		int count = group.activeCount();
		Thread[] threads;
		do {
			threads = new Thread[count + (count / 2) + 1]; // slightly grow the array size
			count = group.enumerate(threads, recurse);
			// return value of enumerate() must be strictly less than the array size
			// according to javadoc
		} while (count >= threads.length);

		final List<Thread> result = new ArrayList<>(count);
		for (int i = 0; i < count; ++i) {
			if (predicate.test(threads[i])) {
				result.add(threads[i]);
			}
		}
		return Collections.unmodifiableCollection(result);
	}

	/**
	 * Select all active threads which match the given predicate.
	 *
	 * @param predicate the predicate
	 * @return An unmodifiable {@code Collection} of active threads matching the
	 *         given predicate
	 *
	 * @throws IllegalArgumentException if the predicate is null
	 * @throws SecurityException        if the current thread cannot access the
	 *                                  system thread group
	 * @throws SecurityException        if the current thread cannot modify thread
	 *                                  groups from this thread's thread group up to
	 *                                  the system thread group
	 */
	public static Collection<Thread> findThreads(final ThreadPredicate predicate) {
		return findThreads(getSystemThreadGroup(), true, predicate);
	}

	/**
	 * Finds active threads with the specified name.
	 *
	 * @param threadName The thread name
	 * @return The threads with the specified name or an empty collection if no such
	 *         thread exists. The collection returned is always unmodifiable.
	 * @throws IllegalArgumentException if the specified name is null
	 * @throws SecurityException        if the current thread cannot access the
	 *                                  system thread group
	 *
	 * @throws SecurityException        if the current thread cannot modify thread
	 *                                  groups from this thread's thread group up to
	 *                                  the system thread group
	 */
	public static Collection<Thread> findThreadsByName(final String threadName) {
		return findThreads(new NamePredicate(threadName));
	}

	/**
	 * Finds active threads with the specified name if they belong to a thread group
	 * with the specified group name.
	 *
	 * @param threadName      The thread name
	 * @param threadGroupName The thread group name
	 * @return The threads which belongs to a thread group with the specified group
	 *         name and the thread's name match the specified name, An empty
	 *         collection is returned if no such thread exists. The collection
	 *         returned is always unmodifiable.
	 * @throws IllegalArgumentException if the specified thread name or group name
	 *                                  is null
	 * @throws SecurityException        if the current thread cannot access the
	 *                                  system thread group
	 *
	 * @throws SecurityException        if the current thread cannot modify thread
	 *                                  groups from this thread's thread group up to
	 *                                  the system thread group
	 */
	public static Collection<Thread> findThreadsByName(final String threadName, final String threadGroupName) {
		Validate.notNull(threadName, "threadName");
		Validate.notNull(threadGroupName, "threadGroupName");

		final Collection<ThreadGroup> threadGroups = findThreadGroups(new NamePredicate(threadGroupName));

		if (threadGroups.isEmpty()) {
			return Collections.emptyList();
		}

		final Collection<Thread> result = new ArrayList<>();
		final NamePredicate threadNamePredicate = new NamePredicate(threadName);
		for (final ThreadGroup group : threadGroups) {
			result.addAll(findThreads(group, false, threadNamePredicate));
		}
		return Collections.unmodifiableCollection(result);
	}

	/**
	 * Finds active threads with the specified name if they belong to a specified
	 * thread group.
	 *
	 * @param threadName  The thread name
	 * @param threadGroup The thread group
	 * @return The threads which belongs to a thread group and the thread's name
	 *         match the specified name, An empty collection is returned if no such
	 *         thread exists. The collection returned is always unmodifiable.
	 * @throws IllegalArgumentException if the specified thread name or group is
	 *                                  null
	 * @throws SecurityException        if the current thread cannot access the
	 *                                  system thread group
	 *
	 * @throws SecurityException        if the current thread cannot modify thread
	 *                                  groups from this thread's thread group up to
	 *                                  the system thread group
	 */
	public static Collection<Thread> findThreadsByName(final String threadName, final ThreadGroup threadGroup) {
		return findThreads(threadGroup, false, new NamePredicate(threadName));
	}

	/**
	 * Gets all active thread groups excluding the system thread group (A thread
	 * group is active if it has been not destroyed).
	 *
	 * @return all thread groups excluding the system thread group. The collection
	 *         returned is always unmodifiable.
	 * @throws SecurityException if the current thread cannot access the system
	 *                           thread group
	 *
	 * @throws SecurityException if the current thread cannot modify thread groups
	 *                           from this thread's thread group up to the system
	 *                           thread group
	 */
	public static Collection<ThreadGroup> getAllThreadGroups() {
		return findThreadGroups(ALWAYS_TRUE_PREDICATE);
	}

	/**
	 * Gets all active threads (A thread is active if it has been started and has
	 * not yet died).
	 *
	 * @return all active threads. The collection returned is always unmodifiable.
	 * @throws SecurityException if the current thread cannot access the system
	 *                           thread group
	 *
	 * @throws SecurityException if the current thread cannot modify thread groups
	 *                           from this thread's thread group up to the system
	 *                           thread group
	 */
	public static Collection<Thread> getAllThreads() {
		return findThreads(ALWAYS_TRUE_PREDICATE);
	}

	/**
	 * Gets the system thread group (sometimes also referred as "root thread
	 * group").
	 *
	 * @return the system thread group
	 * @throws SecurityException if the current thread cannot modify thread groups
	 *                           from this thread's thread group up to the system
	 *                           thread group
	 */
	public static ThreadGroup getSystemThreadGroup() {
		ThreadGroup threadGroup = Thread.currentThread().getThreadGroup();
		while (threadGroup.getParent() != null) {
			threadGroup = threadGroup.getParent();
		}
		return threadGroup;
	}

	/**
	 * Waits for the given thread to die for the given duration. Implemented using
	 * {@link Thread#join(long, int)}.
	 *
	 * @param thread   The thread to join.
	 * @param duration How long to wait.
	 * @throws InterruptedException if any thread has interrupted the current
	 *                              thread.
	 * @see Thread#join(long, int)
	 * @since 3.12.0
	 */
	public static void join(final Thread thread, final Duration duration) throws InterruptedException {
		DurationUtils.accept(thread::join, duration);
	}

	/**
	 * Sleeps the current thread for the given duration. Implemented using
	 * {@link Thread#sleep(long, int)}.
	 *
	 * @param duration How long to sleep.
	 * @throws InterruptedException if any thread has interrupted the current
	 *                              thread.
	 * @see Thread#sleep(long, int)
	 * @since 3.12.0
	 */
	public static void sleep(final Duration duration) throws InterruptedException {
		DurationUtils.accept(Thread::sleep, duration);
	}

	/**
	 * <p>
	 * ThreadUtils instances should NOT be constructed in standard programming.
	 * Instead, the class should be used as {@code ThreadUtils.getAllThreads()}
	 * </p>
	 * <p>
	 * This constructor is public to permit tools that require a JavaBean instance
	 * to operate.
	 * </p>
	 */
	public ThreadUtils() {
	}
}
