// Copyright 2011 The Bazel Authors. All Rights Reserved.
//
// Licensed 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 com.google.testing.junit.runner.internal;

import java.io.PrintStream;
import java.lang.management.LockInfo;
import java.lang.management.ManagementFactory;
import java.lang.management.MonitorInfo;
import java.lang.management.ThreadInfo;
import java.lang.management.ThreadMXBean;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;

/**
 * Utilities for stack traces.
 */
public class StackTraces {

  /**
   * Prints all stack traces to the given stream.
   *
   * @param out Stream to print to
   */
  public static void printAll(PrintStream out) {
    out.println("Starting full thread dump ...\n");
    ThreadMXBean mb = ManagementFactory.getThreadMXBean();

    // ThreadInfo has comprehensive information such as locks.
    ThreadInfo[] threadInfos = mb.dumpAllThreads(true, true);

    // But we can know whether a thread is daemon only from Thread
    Set<Thread> threads = Thread.getAllStackTraces().keySet();
    Map<Long, Thread> threadMap = new HashMap<>();
    for (Thread thread : threads) {
      threadMap.put(thread.getId(), thread);
    }

    // Dump non-daemon threads first
    for (ThreadInfo threadInfo : threadInfos) {
      Thread thread = threadMap.get(threadInfo.getThreadId());
      if (thread != null && !thread.isDaemon()) {
        dumpThreadInfo(threadInfo, thread, out);
      }
    }

    // Dump daemon threads
    for (ThreadInfo threadInfo : threadInfos) {
      Thread thread = threadMap.get(threadInfo.getThreadId());
      if (thread != null && thread.isDaemon()) {
        dumpThreadInfo(threadInfo, thread, out);
      }
    }

    long[] deadlockedThreads = mb.findDeadlockedThreads();
    if (deadlockedThreads != null) {
      out.println("Detected deadlocked threads: " +
                Arrays.toString(deadlockedThreads));
    }
    long[] monitorDeadlockedThreads = mb.findMonitorDeadlockedThreads();
    if (monitorDeadlockedThreads != null) {
      out.println("Detected monitor deadlocked threads: " +
                  Arrays.toString(monitorDeadlockedThreads));
    }
    out.println("\nDone full thread dump.");
    out.flush();
  }

  // Adopted from ThreadInfo.toString(), without MAX_FRAMES limit
  private static void dumpThreadInfo(ThreadInfo t, Thread thread, PrintStream out) {
    out.print("\"" + t.getThreadName() + "\"" +
              " Id=" + t.getThreadId() + " " +
              t.getThreadState());
    if (t.getLockName() != null) {
      out.print(" on " + t.getLockName());
    }
    if (t.getLockOwnerName() != null) {
      out.print(" owned by \"" + t.getLockOwnerName() +
                "\" Id=" + t.getLockOwnerId());
    }
    if (t.isSuspended()) {
      out.print(" (suspended)");
    }
    if (t.isInNative()) {
      out.print(" (in native)");
    }
    if (thread.isDaemon()) {
      out.print(" (daemon)");
    }
    out.print('\n');
    StackTraceElement[] stackTrace = t.getStackTrace();
    MonitorInfo[] lockedMonitors = t.getLockedMonitors();
    for (int i = 0; i < stackTrace.length; i++) {
      StackTraceElement ste = stackTrace[i];
      out.print("\tat " + ste.toString());
      out.print('\n');
      if (i == 0 && t.getLockInfo() != null) {
        Thread.State ts = t.getThreadState();
        switch (ts) {
          case BLOCKED:
            out.print("\t-  blocked on " + t.getLockInfo());
            out.print('\n');
            break;
          case WAITING:
            out.print("\t-  waiting on " + t.getLockInfo());
            out.print('\n');
            break;
          case TIMED_WAITING:
            out.print("\t-  waiting on " + t.getLockInfo());
            out.print('\n');
            break;
          default:
        }
      }

      for (MonitorInfo mi : lockedMonitors) {
        if (mi.getLockedStackDepth() == i) {
          out.print("\t-  locked " + mi);
          out.print('\n');
        }
      }
    }

    LockInfo[] locks = t.getLockedSynchronizers();
    if (locks.length > 0) {
      out.print("\n\tNumber of locked synchronizers = " + locks.length);
      out.print('\n');
      for (LockInfo li : locks) {
        out.print("\t- " + li);
        out.print('\n');
      }
    }
    out.print('\n');
  }
}
