/*
 * Copyright (c) 1998-2012 Caucho Technology -- all rights reserved
 *
 * This file is part of Resin(R) Open Source
 *
 * Each copy or derived work must preserve the copyright notice and this
 * notice unmodified.
 *
 * Resin Open Source is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * Resin Open Source is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE, or any warranty
 * of NON-INFRINGEMENT.  See the GNU General Public License for more
 * details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Resin Open Source; if not, write to the
 *
 *   Free Software Foundation, Inc.
 *   59 Temple Place, Suite 330
 *   Boston, MA 02111-1307  USA
 *
 * @author Scott Ferguson
 */

package com.caucho.util;

import java.lang.management.*;
import java.util.*;
import java.util.concurrent.atomic.AtomicReference;
import java.util.logging.Logger;

import com.caucho.config.ConfigException;

/**
 * Generate a thread dump
 */
public class ThreadDump
{
  private static Logger log = Logger.getLogger(ThreadDump.class.getName());

  private static AtomicReference<ThreadDump> _threadDumpRef = 
    new AtomicReference<ThreadDump>();
  
  /**
   * Returns the singleton instance, creating if necessary.   An instance of 
   * com.caucho.server.admin.ProThreadDump will be returned if available and 
   * licensed.  ProThreadDump includes the URI of the request the thread is
   * processing, if applicable.
   */
  public static ThreadDump create()
  {
    ThreadDump threadDump = _threadDumpRef.get();
    
    if (threadDump == null) {
      try {
        ClassLoader loader = Thread.currentThread().getContextClassLoader();
        Class<?> threadDumpClass = 
          Class.forName("com.caucho.server.admin.ProThreadDump", false, loader);
        threadDump = (ThreadDump) threadDumpClass.newInstance();
      } catch (ClassNotFoundException e) {
        threadDump = new ThreadDump();
      } catch (ConfigException e) {
        threadDump = new ThreadDump();
      } catch (Exception e) {
        throw ConfigException.create(e);
      }
      
      _threadDumpRef.compareAndSet(null, threadDump);
      threadDump = _threadDumpRef.get();
    }
    
    return threadDump;
  }
  
  protected ThreadDump()
  {
    
  }
  
  /**
   * Log all threads to com.caucho.util.ThreadDump at info level.  Uses cached 
   * dump if recent (30s).
   */
  public void dumpThreads()
  {
    log.info(getThreadDump());
  }
  
  /**
   * Returns dump of all threads.  Uses cached dump if recent (30s).
   */
  public String getThreadDump()
  {
    return getThreadDump(32, false);
  }

  /**
   * Log threads to com.caucho.util.ThreadDump at info level.  Optionally 
   * uses cached dump.
   * @param onlyActive if true only running threads are logged
   */
  public void dumpThreads(int depth, boolean onlyActive)
  {
    log.info(getThreadDump(depth, onlyActive));
  }

  /**
   * Returns dump of threads.  Optionally uses cached dump.
   * @param onlyActive if true only running threads are logged
   */
  public String getThreadDump(int depth, boolean onlyActive)
  {
    ThreadMXBean threadBean = ManagementFactory.getThreadMXBean();

    long []ids = threadBean.getAllThreadIds();
    ThreadInfo []info = threadBean.getThreadInfo(ids, depth);

    StringBuilder sb = new StringBuilder();
    sb.append("Thread Dump generated " + new Date(CurrentTime.getCurrentTime()));

    Arrays.sort(info, new ThreadCompare());
    
    buildThreads(sb, info, Thread.State.RUNNABLE, false);
    buildThreads(sb, info, Thread.State.RUNNABLE, true);
    
    if (! onlyActive) {
      buildThreads(sb, info, Thread.State.BLOCKED, false);
      buildThreads(sb, info, Thread.State.WAITING, false);
      buildThreads(sb, info, Thread.State.TIMED_WAITING, false);
      buildThreads(sb, info, null, false);
    }

    return sb.toString();
  }

  protected void buildThreads(StringBuilder sb,
                              ThreadInfo []infoArray,
                              Thread.State matchState,
                              boolean isNative)
  {
    for (int i = 0; i < infoArray.length; i++) {
      ThreadInfo info = infoArray[i];

      if (info == null)
        continue;
      
      ThreadInfo nextInfo = i + 1 < infoArray.length ? infoArray[i + 1] : null;

      Thread.State state = info.getThreadState();

      if (matchState == Thread.State.RUNNABLE
          && (isNative != info.isInNative())) {
        continue;
      }

      if (state == matchState) {
        buildThread(sb, info, nextInfo);
      }
      else if (state == null
               && matchState != Thread.State.RUNNABLE
               && matchState != Thread.State.BLOCKED
               && matchState != Thread.State.WAITING
               && matchState != Thread.State.TIMED_WAITING) {
        buildThread(sb, info, nextInfo);
      }
    }
  }

  protected void buildThread(StringBuilder sb, 
                             ThreadInfo info,
                             ThreadInfo nextInfo)
  {
    sb.append("\n\"");
    sb.append(info.getThreadName());
    sb.append(" " + info.getThreadState());

    if (info.isInNative())
      sb.append(" (in native)");

    if (info.isSuspended())
      sb.append(" (suspended)");

    String lockName = info.getLockName();
    if (lockName != null) {
      sb.append("\n    waiting on ");
      sb.append(lockName);

      if (info.getLockOwnerName() != null) {
        sb.append("\n    owned by \"");
        sb.append(info.getLockOwnerName());
        sb.append("\"");
      }
    }

    sb.append("\n");
    
    if (nextInfo != null
        && threadCmpString(info).equals(threadCmpString(nextInfo)))
      return;

    StackTraceElement []stackList = info.getStackTrace();
    if (stackList == null)
      return;

    for (StackTraceElement stack : stackList) {
      sb.append("  at ");
      sb.append(stack.getClassName());
      sb.append(".");
      sb.append(stack.getMethodName());

      if (stack.getFileName() != null) {
        sb.append(" (");
        sb.append(stack.getFileName());
        if (stack.getLineNumber() > 0) {
          sb.append(":");
          sb.append(stack.getLineNumber());
        }
        sb.append(")");
      }

      if (stack.isNativeMethod())
        sb.append(" (native)");

      sb.append("\n");
    }
  }

  public String jsonThreadDump()
  {
    StringBuilder sb = new StringBuilder();
    
    long timestamp = CurrentTime.getCurrentTime();
    
    sb.append("{\n");
    sb.append("  \"create_time\": \"" + new Date(timestamp) + "\",\n");
    sb.append("  \"timestamp\": " + timestamp + ",\n");
    sb.append("  \"thread_dump\" : {\n");
    
    ThreadMXBean threadBean = ManagementFactory.getThreadMXBean();

    long []ids = threadBean.getAllThreadIds();
    ThreadInfo []infoList = threadBean.getThreadInfo(ids, true, true);
    
    boolean isFirst = true;

    for (ThreadInfo threadInfo : infoList) {
      if (threadInfo == null)
        continue;
      
      if (! isFirst) {
        sb.append(",\n");
      }
      isFirst = false;
      
      jsonDumpThread(sb, threadInfo);
    }
    
    sb.append("\n  }");
    sb.append("\n}");

    return sb.toString();
  }
  
  private void jsonDumpThread(StringBuilder sb, ThreadInfo info)
  {
    sb.append("\"" + info.getThreadId() + "\" : {");
    
    sb.append("\n  \"id\" : " + info.getThreadId());
    
    sb.append(",\n  \"name\" : \"");
    escapeString(sb, info.getThreadName());
    sb.append("\"");
    
    /*
    sb.append(",\n  \"description\" : \"");
    escapeString(sb, info.getDescription());
    sb.append("\"");
    */
    
    sb.append(",\n  \"state\" : \"" + info.getThreadState() + "\"");
    
    if (info.isInNative()) {
      sb.append(",\n  \"native\" : true");
    }
    
    if (info.getLockName() != null) {
      sb.append(",\n  \"lock\" : {");
      
      sb.append("\n    \"name\" : \"");
      escapeString(sb, info.getLockName());
      sb.append("\"");
      
      sb.append(",\n    \"owner_id\" : " + info.getLockOwnerId());
      
      if (info.getLockOwnerName() != null) {
        sb.append(",\n    \"owner_name\" : \"");
        escapeString(sb, info.getLockOwnerName());
        sb.append("\"");
      }
      
      sb.append("\n  }");
    }
    
    jsonDumpStackTrace(sb, info.getStackTrace());
    jsonDumpMonitors(sb, info.getLockedMonitors())
    ;
    sb.append("\n}");
  }

  private void jsonDumpStackTrace(StringBuilder sb,
                                  StackTraceElement []stackTrace)
  {
    if (stackTrace == null)
      return;
    
    sb.append(",\n  \"stack\" : [\n");
    
    for (int i = 0; i < stackTrace.length; i++) {
      StackTraceElement elt = stackTrace[i];
      
      if (i != 0)
        sb.append(",\n");
      
      sb.append("  {");
      
      sb.append("\n    \"class\" : \"" + elt.getClassName() + "\"");
      sb.append(",\n    \"method\" : \"" + elt.getMethodName() + "\"");
      
      if (elt.getFileName() != null) {
        sb.append(",\n    \"file\" : \"" + elt.getFileName() + "\"");
        sb.append(",\n    \"line\" : \"" + elt.getLineNumber() + "\"");
      }
      
      if (elt.isNativeMethod())
        sb.append(",\n    \"native\" : true");
      
      sb.append("\n  }");
    }
    
    sb.append("]");
  }
  
  private void jsonDumpMonitors(StringBuilder sb, 
                                MonitorInfo[] lockedMonitors)
  {
    if (lockedMonitors == null || lockedMonitors.length == 0)
      return;
    
    sb.append(",\n  \"monitors\" : [\n");
    
    for (int i = 0; i < lockedMonitors.length; i++) {
      MonitorInfo info = lockedMonitors[i];
      
      if (i != 0)
        sb.append(",\n");
      
      sb.append("  {\n");
      
      sb.append("    \"depth\" : " + info.getLockedStackDepth());
      sb.append(",\n    \"class\" : \"" + info.getClassName() + "\"");
      sb.append(",\n    \"hash\" : \"" + info.getIdentityHashCode() + "\"");
      
      sb.append("  }");
    }
    
    sb.append("\n  ]");
  }
  
  private void escapeString(StringBuilder sb, String value)
  {
    int len = value.length();
    
    for (int i = 0; i < len; i++) {
      char ch = value.charAt(i);
      
      switch (ch) {
      case '"':
        sb.append("\\\"");
        break;
      case '\\':
        sb.append("\\\\");
        break;
      default:
        sb.append(ch);
      }
    }
  }
  
  static String threadCmpString(ThreadInfo info)
  {
    if (info == null)
      return "";
    
    StackTraceElement []stackList = info.getStackTrace();
    
    if (stackList == null)
      return "";
    
    StringBuilder sb = new StringBuilder();
    
    if (info.getThreadState() == Thread.State.RUNNABLE)
      sb.append("A-RUNNABLE");
    else
      sb.append(info.getThreadState());
    
    sb.append(" " + info.isInNative());
    
    for (int i = stackList.length - 1; i >= 0; i--) {
      sb.append("\n").append(stackList[i].getClassName());
      sb.append(".").append(stackList[i].getMethodName());
      
      if (stackList[i].getFileName() != null) {
        sb.append("(").append(stackList[i].getFileName());
        sb.append(".").append(stackList[i].getLineNumber());
        sb.append(")");
      }
    }
    
    return sb.toString();
    
  }

  static class ThreadCompare implements Comparator<ThreadInfo> {
    public int compare(ThreadInfo a, ThreadInfo b)
    {
      if (a == b)
        return 0;
      else if (a == null)
        return -1;
      else if (b == null)
        return 1;
      
      String cmpA = threadCmpString(a);
      String cmpB = threadCmpString(b);
      
      int cmp = cmpA.compareTo(cmpB);
      
      if (cmp != 0)
        return cmp;
      
      return a.getThreadName().compareTo(b.getThreadName());
    }
  }
}
