/*
 * JBoss, Home of Professional Open Source.
 * See the COPYRIGHT.txt file distributed with this work for information
 * regarding copyright ownership.  Some portions may be licensed
 * to Red Hat, Inc. under one or more contributor license agreements.
 * 
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 * 
 * This library 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
 * Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
 * 02110-1301 USA.
 * Modified by
 * @author  Data Virtuality GmbH
 * @date    $Date$
 *
 */
// https://issues.jboss.org/browse/TEIID-2813

package org.teiid.dqp.internal.datamgr;

import com.datavirtuality.dv.core.interfaces.DVCaller;
import java.lang.management.ManagementFactory;
import java.lang.management.ThreadMXBean;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
import org.jboss.logging.Logger;

/**
 * Timer class that uses the ThreadMXBean for CPU timing
 */
public class ThreadCpuTimer {

	private static ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
	protected static ThreadLocal<TimerContext> threadLocalTimerContext = new ThreadLocal<TimerContext>() {
		@Override
		protected TimerContext initialValue() {
			return new TimerContext();
		}
	};
	private AtomicLong totalTime = new AtomicLong(-1);
	protected AtomicLong totalHeapAllocated = new AtomicLong(-1);
	private AtomicLong elapsedTime = new AtomicLong();
	private volatile long lastStatCpuTime;
	private volatile long lastStatHeap;
	private int nCPUs = Runtime.getRuntime().availableProcessors();
	private boolean active;
	protected boolean heapActive;

	private static Logger logger = Logger.getLogger(ThreadCpuTimer.class);

	public ThreadCpuTimer() {
		active = threadMXBean.isThreadCpuTimeSupported()
				&& threadMXBean.isThreadCpuTimeEnabled();
	}

	public void start() {
		if (!active) {
			return;
		}
		TimerContext context = threadLocalTimerContext.get();
		context.setThread(Thread.currentThread());
		try {
			context.setLastStatUpTime(System.currentTimeMillis());
			context.setLastTime(threadMXBean.getThreadCpuTime(context.getThreadId()));
		} catch (UnsupportedOperationException e) {
			inactivate();
		}

	}

	private void inactivate() {
		active = false;
		threadLocalTimerContext.get().setLastTime(-1);
		totalTime.set(-1);
	}

	public long stop() {
		if (!active) {
			return -1;
		}
		TimerContext context = threadLocalTimerContext.get();
		if (context.getLastTime() != -1) {
			try {
				long upTime = System.currentTimeMillis();
				long time = threadMXBean.getThreadCpuTime(context.getThreadId());
				if (time != -1) {
					if (totalTime.get() == -1) {
						totalTime.set(0);
					}
					long consumedTime = time - context.getLastTime();
					totalTime.getAndAdd(TimeUnit.NANOSECONDS.toMicros(consumedTime));
					setElapsedTime(upTime, context);
				} else {
					inactivate();
				}
			} catch (UnsupportedOperationException e) {
				inactivate();
			}
		}
		context.setLastTime(-1);
		return totalTime.get();
	}

	public CPUStats getCPUStats() {
		long totalHeap = this.totalHeapAllocated.get();
		if (!active) {
			return new CPUStats(totalHeap);
		}
		long totalTime = this.totalTime.get();
		return new CPUStats(totalTime > 0 ? totalTime : 0, totalHeap);
	}

	private void setElapsedTime(long upTime, TimerContext context) {
		elapsedTime.getAndAdd(upTime - context.getLastStatUpTime());
		context.setLastStatUpTime(upTime);
	}

	public static ThreadCpuTimer createTimer() {
		ThreadCpuTimer result = (ThreadCpuTimer) DVCaller.call(116);
		return result != null ? result : new ThreadCpuTimer();
	}

	public class CPUStats {
		/**
		 * Total consumed cpu time in microseconds
		 */
		private long cpuTime;

		/**
		 * cpu usage in percentages
		 */
		private float cpuUsage;

		/**
		 * Total consumed heap memory by current thread
		 */
		private long heapAllocated;

		public CPUStats() {
			this.cpuTime = -1L;
			this.cpuUsage = -1F;
			this.heapAllocated = -1L;
		}

		public CPUStats(long heapAllocated) {
			this();
			this.heapAllocated = getHeapAllocated(heapAllocated);
		}

		public CPUStats(long cpuTime, long heapAllocated) {
			this.cpuUsage = getCpuUsage(cpuTime);
			this.heapAllocated = getHeapAllocated(heapAllocated);
		}

		public long getCpuTime() {
			return cpuTime;
		}

		public float getCpuUsage() {
			return cpuUsage;
		}

		public long getHeapAllocated() {
			return heapAllocated;
		}

		private float getCpuUsage(long cpuTime) {
			// elapsedTime is in ms
			if (elapsedTime.get() > 0L) {
				// elapsedCpu is in micros
				this.cpuTime = cpuTime - lastStatCpuTime;
				// elapsedCpu is in ns
				long elapsedCpu = TimeUnit.MICROSECONDS.toNanos(this.cpuTime);
				float cpuUsage = (float) Math.round(Math.min(999F, elapsedCpu / (elapsedTime.get() * 1000F * nCPUs))) / 10;
				lastStatCpuTime = cpuTime;
				elapsedTime.set(0L);

				return cpuUsage;
			}

			return 0F;
		}

		private long getHeapAllocated(long heap) {
			if (!heapActive)
				return -1L;

			if (heap > 0L) {
				long result = heap - lastStatHeap;
				lastStatHeap = heap;
				return result;
			}

			return 0L;
		}
	}

	protected static class TimerContext {
		private Thread thread;
		private long lastTime = -1;
		private long lastHeapAllocated = -1;
		private long lastStatUpTime;

		public String getThreadName() {
			return thread != null ? thread.getName() : null;
		}

		public long getThreadId() {
			return thread != null ? thread.getId() : -1;
		}

		public void setThread(Thread thread) {
			this.thread = thread;
		}

		public Thread getThread() {
			return thread;
		}

		public void setLastTime(long lastTime) {
			this.lastTime = lastTime;
		}

		public long getLastTime() {
			return lastTime;
		}

		public void setLastHeapAllocated(long lastHeapAllocated) {
			this.lastHeapAllocated = lastHeapAllocated;
		}

		public long getLastHeapAllocated() {
			return lastHeapAllocated;
		}

		public long getLastStatUpTime() {
			return lastStatUpTime;
		}

		public void setLastStatUpTime(long lastStatUpTime) {
			this.lastStatUpTime = lastStatUpTime;
		}
	}
}