/*
 * Copyright (c) 1997, 2018, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code 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.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */

package org.graalvm.visualvm.lib.jfluid.server;

import java.util.ArrayList;
import java.util.List;

/**
 * An instance of this class is allocated for each profiled thread, to hold thread-local information, such as whether
 * the thread is currently in the profiled call subgraph, the simulated call stack, the thread-local rough generated
 * data buffer (for CPU profiling), etc. Static methods to create and lookup instances of ThreadInfo given a Thread
 * object are also provided in this class.
 *
 * @author Tomas Hurka
 * @author Misha Dmitriev
 */
public class ThreadInfo {
    //~ Static fields/initializers -----------------------------------------------------------------------------------------------

    static final int MAX_EVENT_ENTRIES_IN_LOCAL_BUFFER = 500; // Thread-local buffer, in events
    static final int MAX_EVENT_SIZE = 1 + 2 + 7 + 7; // In bytes; comprises event type, method id, and two long timestamps
    static int evBufSize; // Size and threshold, same for each thread-local buffer
    static int evBufPosThreshold; // Size and threshold, same for each thread-local buffer

    static {
        setDefaultEvBufParams();
    }

    static Thread[] profilerServerThreads;
    static int nProfilerServerThreads;
    static int nProfiledAppThreads;
    static ThreadInfo dummyThreadInfo = new ThreadInfo(null); // Used just to avoid null checks in some situations
    private static boolean profilingSuspended = false;

    // ThreadInfo hash table
    private static ThreadInfo[] threadInfos = new ThreadInfo[1]; // To avoid null checks - important!
    private static final Object threadInfosLock = new Object();
    private static int threadInfosSize;
    private static int nThreads;
    private static boolean hasDeadThreads;
    private static ThreadInfo lastThreadInfo = dummyThreadInfo;

    //~ Instance fields ----------------------------------------------------------------------------------------------------------

    Thread thread; // Thread object for this ThreadInfo
    byte[] evBuf; // Thread-local event (rough profiling data) buffer. Currently used in CPU profiling only.
    boolean inCallGraph; // Indicates whether the thread is currently in the profiled subgraph
    boolean sampleDue; // In sampled instrumentation mode, indicates that next sampling should be done
    int evBufDumpLastPos; // Used to avoid synchronization in writeEvent() and yet to allow for asynchronous event buffer dumps.
    int evBufPos; // Current position in the local event buffer
    int inProfilingRuntimeMethod; // Indicates whether currently some profiling runtime method is executed on behalf of this thread
    int rootMethodStackDepth; // logical stack depth of the root method which is inside of marker method
    int stackDepth; // Current logical (i.e. relative to the root method frame) stack depth
    int threadId; // Integer ID
    long absEntryTime; // Used to support thread suspension and code fragment profiling
    long lastWaitStartTime; // Used in Code Region profiling for tracking wait times
    long threadEntryTime; // Used to support thread suspension and code fragment profiling
    private boolean initialized; // To signal that this thread is not initialized or was reset, so this threadInfo is unusable
    List parameters;

    //~ Constructors -------------------------------------------------------------------------------------------------------------

    private ThreadInfo(Thread thread) {
        this.thread = thread;
        inProfilingRuntimeMethod = 1; // To make possible trace method calls while ThreadInfo is initialized return immediately
        threadId = nThreads & 0xFFFF;
    }

    //~ Methods ------------------------------------------------------------------------------------------------------------------

    public final boolean isInCallGraph() {
        return inCallGraph;
    }

    public final Thread getThread() {
        return thread;
    }

    public final int getThreadId() {
        return threadId;
    }

    static boolean isProfilerServerMonitor(Object monitor) {
        return monitor == threadInfosLock || monitor == ProfilerRuntimeMemory.classIdMapLock;
    }
    
    public static boolean isCurrentThreadProfilerServerThread() {
        return isProfilerServerThread(Thread.currentThread());
    }

    public static boolean isProfilerServerThread(Thread thread) {
        if (profilerServerThreads == null) {
            return false;
        }

        for (int i = 0; i < nProfilerServerThreads; i++) {
            if (profilerServerThreads[i] == thread) {
                return true;
            }
        }

        return false;
    }

    public static synchronized void addProfilerServerThread(Thread thread) {
        profilerServerThreads[nProfilerServerThreads++] = thread;
    }

    //-----------------------------------------------------------------------------------------------
    // Accounting for profiler's own threads
    //-----------------------------------------------------------------------------------------------

    // We use a simple array of Threads, not a Vector (as once before) here, since Vector's "contains()" method's
    // performance is really worse than that of simple compare in isProfilerThread() below. It's likely related to the
    // fact that contains() calls one or two other methods and uses "equals()" for compares, rather than simple "==".
    // Otherwise these methods don't have to be very sophisticated, since there are not going to be more than 3-6 such
    // threads. Also, during CPU profiling each of them is called only once, when a ThreadInfo is created for the
    // corresponding thread. A field is marked accordingly in ThreadInfo, and then checked in each methodEntry/Exit().
    public static synchronized void clearProfilerServerThreads() {
        if (profilerServerThreads == null) {
            profilerServerThreads = new Thread[10];
        } else {
            for (int i = 0; i < nProfilerServerThreads; i++) {
                profilerServerThreads[i] = null;
            }

            nProfilerServerThreads = 0;
        }
    }

    public static boolean profilingSuspended() {
        return profilingSuspended;
    }

    public static synchronized void removeProfilerServerThread(Thread thread) {
        if (profilerServerThreads == null) {
            return;
        }

        for (int i = 0; i < nProfilerServerThreads; i++) {
            if (profilerServerThreads[i] == thread) {
                if (i < (nProfilerServerThreads - 1)) {
                    System.arraycopy(profilerServerThreads, i + 1, profilerServerThreads, i, nProfilerServerThreads - i - 1);
                }
                profilerServerThreads[nProfilerServerThreads - 1] = null;

                nProfilerServerThreads--;

                return;
            }
        }
    }

    static int getLiveServerThreads() {
        int serverThreads = 0;
        for (int i = 0; i < nProfilerServerThreads; i++) {
            if (profilerServerThreads[i].isAlive()) {
                serverThreads++;
            }
        }
        return serverThreads;
    }

    public static void resumeProfiling() {
        profilingSuspended = false;
    }

    public static void suspendProfiling() {
        profilingSuspended = true;
    }

    static byte[] getCurrentLivenessStatus() {
        ThreadInfo[] tis = threadInfos;
        int resLen = nThreads;
        byte[] res = new byte[resLen];

        for (int i = 0; i < tis.length; i++) {
            ThreadInfo ti = tis[i];

            if ((ti != null) && (ti.threadId < resLen) && (ti.thread != null)) {
                // We don't care if a new thread was created, but we don't list it - at the tool side the currently observed
                // number of threads is also <= nThreads
                res[ti.threadId] = ti.thread.isAlive() ? (byte) 1 : 0;
            }
        }

        return res;
    }

    static void setDefaultEvBufParams() {
        evBufSize = MAX_EVENT_ENTRIES_IN_LOCAL_BUFFER * MAX_EVENT_SIZE;
        evBufPosThreshold = evBufSize - (4 * MAX_EVENT_SIZE) - 1;
        threadInfos = new ThreadInfo[1]; // To avoid null checks
        threadInfosSize = 0;
    }

    final boolean isInitialized() {
        return initialized;
    }

    final void initialize() {
        inProfilingRuntimeMethod++;

        if (!isProfilerServerThread(thread)) {
            nProfiledAppThreads++;
        }

        resetInternalState();
        initialized = true;
        inProfilingRuntimeMethod--;
    }

    final void useEventBuffer() {
        evBuf = new byte[evBufSize];
    }

    void addParameter(Object par) {
        if (parameters == null) {
            parameters = new ArrayList();
        }
        parameters.add(par);
    }
    
    List getParameters() {
        return parameters;
    }
    
    void clearParameters() {
        parameters.clear();
    }
    
    static int getNProfiledAppThreads() {
        return nProfiledAppThreads;
    }

    static void setSampleDueForAllThreads() {
        ThreadInfo[] tis = threadInfos;

        for (int i = 0; i < tis.length; i++) {
            ThreadInfo ti = tis[i];

            if (ti != null) { // We don't care if a new thread was created, but we don't list it
                ti.sampleDue = true;
            }
        }
    }

    static ThreadInfo getThreadInfo() {
        Thread thread = Thread.currentThread();
        ThreadInfo ti = lastThreadInfo;

        if (ti.thread == thread) {
            return ti;
        }

        return getThreadInfo(thread);
    }

    static ThreadInfo getThreadInfo(Thread thread) {
        ThreadInfo ti = getThreadInfoOrNull(thread);

        if (ti == null) {
            ti = newThreadInfo(thread);
        }

        return ti;
    }

    static ThreadInfo[] getThreadInfos() {
        return threadInfos;
    }

    static void changeAllThreadsInProfRuntimeMethodStatus(int val) {
        synchronized (threadInfosLock) {
            for (int i = 0; i < threadInfos.length; i++) {
                ThreadInfo ti = threadInfos[i];

                if (!ti.inCallGraph) {
                    continue;
                }

                ti.inProfilingRuntimeMethod += val;
            }
        }
    }

    static void resetThreadInfoTable() {
        ThreadInfo[] oldTIs = threadInfos;

        synchronized (threadInfosLock) {
            nProfiledAppThreads = 0;
            lastThreadInfo = dummyThreadInfo; // To avoid null checks

            for (int i = 0; i < oldTIs.length; i++) {
                ThreadInfo ti = oldTIs[i];

                if ((ti == null) || (ti.thread == null)) {
                    oldTIs[i] = null;

                    continue;
                }

                ti.initialized = false;
            }
        }
    }

    //-----------------------------------------------------------------------------------------------
    // Special methods for non-standard usage of ThreadInfo
    //-----------------------------------------------------------------------------------------------

    // This method is used only by ProfilerCalibrator
    void setEvBuf(byte[] buf) {
        evBuf = buf;
        evBufSize = buf.length;
        evBufPosThreshold = buf.length - (2 * MAX_EVENT_SIZE) - 1;
    }

    static void releaseDeadThreads() {
        ThreadInfo[] tis = threadInfos;

        for (int i = 0; i < tis.length; i++) {
            ThreadInfo ti = tis[i];

            if (ti != null) {
                Thread t = ti.thread;

                if ((t != null) && !t.isAlive()) {
                    if (ti.evBuf != null) {
                        if (ti.evBufPos > 0) { // dump local event buffer
                            ProfilerRuntimeCPU.copyLocalBuffer(ti);
                        }
                        ti.evBuf = null; // release results buffer
                    }
                    ti.thread = null; // release dead thread
                    ti.parameters = null;
                    hasDeadThreads = true;
                }
            }
        }
    }

    private static int getThreadHashCode(Thread t) {
        return System.identityHashCode(t) & 0x7fffffff;
    }

    private static ThreadInfo getThreadInfoOrNull(Thread thread) {
        ThreadInfo[] tis = threadInfos;
        int capacity = tis.length;
        int pos = getThreadHashCode(thread) % capacity;
        ThreadInfo ti;

        while ((ti = tis[pos]) != null) {
            if (ti.thread == thread) {
                return ti;
            } else {
                pos = (pos + 1) % capacity;
            }
        }

        return null;
    }

    private static void addThreadInfo(final ThreadInfo res, final Thread thread) {
        if (threadInfosSize >= ((threadInfos.length * 3) / 4)) {
            rehash();
        }

        int capacity = threadInfos.length;
        int pos = getThreadHashCode(thread) % capacity;

        while (threadInfos[pos] != null) {
            pos = (pos + 1) % capacity;
        }

        threadInfos[pos] = res;
        threadInfosSize++;
    }

    private static ThreadInfo newThreadInfo(Thread thread) {
        synchronized (threadInfosLock) {
            ThreadInfo ti = getThreadInfoOrNull(thread);

            if (ti != null) {
                return ti;
            }

            ThreadInfo res = new ThreadInfo(thread);

            nThreads++;
            addThreadInfo(res, thread);
            res.inProfilingRuntimeMethod = 0;

            return res;
        }
    }

    private static void rehash() {
        int capacity = hasDeadThreads ? threadInfos.length : (threadInfos.length * 2) + 1;
        ThreadInfo[] newTIs = new ThreadInfo[capacity];
        int size = 0;

        for (int i = 0; i < threadInfos.length; i++) {
            ThreadInfo ti = threadInfos[i];

            if ((ti == null) || (ti.thread == null)) {
                continue;
            }

            int pos = getThreadHashCode(ti.thread) % capacity;

            while (newTIs[pos] != null) {
                pos = (pos + 1) % capacity;
            }

            newTIs[pos] = ti;
            size++;
        }

        threadInfos = newTIs;
        threadInfosSize = size;
        hasDeadThreads = false;
    }

    private void resetInternalState() {
        evBufPos = evBufDumpLastPos = 0;
        absEntryTime = lastWaitStartTime = threadEntryTime = 0;
        rootMethodStackDepth = stackDepth = 0;
        inCallGraph = sampleDue = false;
        evBuf = null;
    }
}
