/*
    PIP - Platform Independent Primitives
    Process resource monitor
    Ivan Pelipenko peri4ko@yandex.ru

    This program 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 3 of the License, or
    (at your option) any later version.

    This program 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 program.  If not, see <http://www.gnu.org/licenses/>.
*/

#include "pisystemmonitor.h"

#include "pidir.h"
#include "piliterals_string.h"
#include "piprocess.h"
#include "pisysteminfo.h"
#include "pitime_win.h"
#include "pitranslator.h"

#include <unistd.h>
#ifdef WINDOWS
#  include <psapi.h>
#  include <tlhelp32.h>
#endif
#ifdef MAC_OS
struct kqueue_id_t;
#  include <libproc.h>
#  include <sys/proc_info.h>
#endif
#ifdef ESP_PLATFORM
#  include "esp_heap_caps.h"
#endif


void PISystemMonitor::ProcessStats::makeStrings() {
	physical_memsize_readable.setReadableSize(physical_memsize);
	resident_memsize_readable.setReadableSize(resident_memsize);
	share_memsize_readable.setReadableSize(share_memsize);
	virtual_memsize_readable.setReadableSize(virtual_memsize);
	data_memsize_readable.setReadableSize(data_memsize);
}


#ifndef MICRO_PIP
PRIVATE_DEFINITION_START(PISystemMonitor)
#  ifndef WINDOWS
#	ifdef MAC_OS
	PISystemTime
#	else
	llong
#	endif
		cpu_u_cur,
		cpu_u_prev, cpu_s_cur, cpu_s_prev;
	PIString proc_dir;
	PIFile file, filem;
#  else
	HANDLE hProc;
	PROCESS_MEMORY_COUNTERS mem_cnt;
	PISystemTime tm_kernel, tm_user;
	PITimeMeasurer tm;
#  endif
PRIVATE_DEFINITION_END(PISystemMonitor)
#endif


PISystemMonitor::PISystemMonitor(): PIThread() {
	pID_ = cycle = 0;
	cpu_count    = PISystemInfo::instance()->processorsCount;
#ifndef MICRO_PIP
#  ifndef WINDOWS
#	ifdef QNX
	page_size = 4096;
#	else
	page_size = getpagesize();
#	endif
#  else
	PRIVATE->hProc      = 0;
	PRIVATE->mem_cnt.cb = sizeof(PRIVATE->mem_cnt);
#  endif
#endif
	setName("system_monitor"_a);
}


PISystemMonitor::~PISystemMonitor() {
	stop();
}


#ifndef MICRO_PIP
bool PISystemMonitor::startOnProcess(int pID, PISystemTime interval) {
	stop();
	pID_ = pID;
	Pool::instance()->add(this);
	cycle = -1;
#  ifndef WINDOWS
#	ifndef MAC_OS
	PRIVATE->proc_dir = PIStringAscii("/proc/") + PIString::fromNumber(pID_) + PIStringAscii("/");
	PRIVATE->file.open(PRIVATE->proc_dir + "stat", PIIODevice::ReadOnly);
	PRIVATE->filem.open(PRIVATE->proc_dir + "statm", PIIODevice::ReadOnly);
	if (!PRIVATE->file.isOpened()) {
		piCoutObj << "Can`t find process with ID = %1!"_tr("PISystemMonitor").arg(pID_);
		return false;
	}
#	endif
#  else
	PRIVATE->hProc = OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, FALSE, pID_);
	if (PRIVATE->hProc == 0) {
		piCoutObj << "Can`t open process with ID = %1, %2!"_tr("PISystemMonitor").arg(pID_).arg(errorString());
		return false;
	}
	PRIVATE->tm.reset();
#  endif
	return start(interval);
}
#endif


bool PISystemMonitor::startOnSelf(PISystemTime interval) {
#ifndef MICRO_PIP
	bool ret = startOnProcess(PIProcess::currentPID(), interval);
	cycle    = -1;
#else
	bool ret = start(interval);
#endif
	return ret;
}


PIVector<PISystemMonitor::ThreadStats> PISystemMonitor::threadsStatistic() const {
	lock();
	PIVector<PISystemMonitor::ThreadStats> ret = cur_ts;
	unlock();
	return ret;
}


void PISystemMonitor::setStatistic(const PISystemMonitor::ProcessStats & s) {
	PIMutexLocker _ml(stat_mutex);
	stat = s;
	stat.makeStrings();
}


void PISystemMonitor::stop() {
	PIThread::stopAndWait();
#ifdef WINDOWS
	if (PRIVATE->hProc != 0) {
		CloseHandle(PRIVATE->hProc);
		PRIVATE->hProc = 0;
	}
#endif
	Pool::instance()->remove(this);
}


PISystemMonitor::ProcessStats PISystemMonitor::statistic() const {
	PIMutexLocker _ml(stat_mutex);
	return stat;
}


#ifdef MAC_OS
PISystemTime uint64toST(uint64_t v) {
	return PISystemTime(((uint *)&(v))[1], ((uint *)&(v))[0]);
}
#endif

void PISystemMonitor::run() {
	cur_tm.clear();
	tbid.clear();
	__PIThreadCollection * pitc = __PIThreadCollection::instance();
	pitc->lock();
	PIVector<PIThread *> tv = pitc->threads();
	for (auto * t: tv)
		if (t->isPIObject()) tbid[t->tid()] = t->name();
	pitc->unlock();
	// piCout << tbid.keys().toType<uint>();
	ProcessStats tstat;
	tstat.ID = pID_;
#ifdef MICRO_PIP
	for (auto * t: tv)
		if (t->isPIObject()) gatherThread(t->tid());
#else
#  ifndef WINDOWS
	double delay_ms = delay_.toMilliseconds();
	tbid[pID_]      = "main";
#	ifdef MAC_OS
	rusage_info_current ru;
	proc_pid_rusage(pID_, RUSAGE_INFO_CURRENT, (rusage_info_t *)&ru);
	// piCout << PISystemTime(((uint*)&(ru.ri_user_time))[1], ((uint*)&(ru.ri_user_time))[0]);
	if (cycle < 0) {
		PRIVATE->cpu_u_prev = PRIVATE->cpu_u_cur = uint64toST(ru.ri_user_time);
		PRIVATE->cpu_s_prev = PRIVATE->cpu_s_cur = uint64toST(ru.ri_system_time);
	}
	PRIVATE->cpu_u_prev   = PRIVATE->cpu_u_cur;
	PRIVATE->cpu_s_prev   = PRIVATE->cpu_s_cur;
	PRIVATE->cpu_u_cur    = uint64toST(ru.ri_user_time);
	PRIVATE->cpu_s_cur    = uint64toST(ru.ri_system_time);
	tstat.cpu_load_system = 100.f * (PRIVATE->cpu_s_cur - PRIVATE->cpu_s_prev).toMilliseconds() / delay_ms;
	tstat.cpu_load_user   = 100.f * (PRIVATE->cpu_u_cur - PRIVATE->cpu_u_prev).toMilliseconds() / delay_ms;
	cycle                 = 0;
	// piCout << (PRIVATE->cpu_u_cur - PRIVATE->cpu_u_prev).toMilliseconds() / delay_ms;
#	else
	PRIVATE->file.seekToBegin();
	PIString str = PIString::fromAscii(PRIVATE->file.readAll());
	int si = str.find('(') + 1, fi = 0, cc = 1;
	for (int i = si; i < str.size_s(); ++i) {
		if (str[i] == '(') cc++;
		if (str[i] == ')') cc--;
		if (cc <= 0) {
			fi = i;
			break;
		}
	}
	tstat.exec_name = str.mid(si, fi - si);
	str.cutMid(si - 1, fi - si + 3);
	PIStringList sl = str.split(" ");
	if (sl.size_s() < 19) return;
	tstat.ID         = sl[0].toInt();
	tstat.state      = sl[1];
	tstat.parent_ID  = sl[2].toInt();
	tstat.group_ID   = sl[3].toInt();
	tstat.session_ID = sl[4].toInt();
	if (cycle < 0) {
		PRIVATE->cpu_u_prev = PRIVATE->cpu_u_cur = sl[12].toLLong();
		PRIVATE->cpu_s_prev = PRIVATE->cpu_s_cur = sl[13].toLLong();
	}
	PRIVATE->cpu_u_prev   = PRIVATE->cpu_u_cur;
	PRIVATE->cpu_s_prev   = PRIVATE->cpu_s_cur;
	PRIVATE->cpu_u_cur    = sl[12].toLLong();
	PRIVATE->cpu_s_cur    = sl[13].toLLong();
	tstat.cpu_load_system = (PRIVATE->cpu_s_cur - PRIVATE->cpu_s_prev) / (delay_ms / 1000.);
	tstat.cpu_load_user   = (PRIVATE->cpu_u_cur - PRIVATE->cpu_u_prev) / (delay_ms / 1000.);
	tstat.cpu_load_system /= cpu_count;
	tstat.cpu_load_user /= cpu_count;
	cycle          = 0;
	tstat.priority = sl[16].toInt();
	tstat.threads  = sl[18].toInt();
	// piCout << "\n";
	// piCout << sl[0] << sl[12] << sl[13];

	PRIVATE->filem.seekToBegin();
	str = PIString::fromAscii(PRIVATE->filem.readAll());
	sl  = str.split(" ");
	if (sl.size_s() < 6) return;
	tstat.virtual_memsize          = sl[0].toLong() * page_size;
	tstat.resident_memsize         = sl[1].toLong() * page_size;
	tstat.share_memsize            = sl[2].toLong() * page_size;
	tstat.data_memsize             = sl[5].toLong() * page_size;
	tstat.physical_memsize         = tstat.resident_memsize - tstat.share_memsize;

	PIVector<PIFile::FileInfo> tld = PIDir(PRIVATE->proc_dir + "task").entries();
	for (const auto & i: tld) {
		if (i.flags[PIFile::FileInfo::Dot] || i.flags[PIFile::FileInfo::DotDot]) continue;
		gatherThread(i.name().toInt());
	}
#	endif
#  else
	if (GetProcessMemoryInfo(PRIVATE->hProc, &PRIVATE->mem_cnt, sizeof(PRIVATE->mem_cnt)) != 0) {
		tstat.physical_memsize = PRIVATE->mem_cnt.WorkingSetSize;
	}
	tstat.priority = GetPriorityClass(PRIVATE->hProc);
	HANDLE snap    = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, pID_);
	int thcnt      = 0;
	if (snap != 0) {
		THREADENTRY32 thread;
		thread.dwSize = sizeof(THREADENTRY32);
		if (Thread32First(snap, &thread) == TRUE) {
			if (thread.th32OwnerProcessID == DWORD(pID_)) {
				++thcnt;
				gatherThread(thread.th32ThreadID);
			}
			while (Thread32Next(snap, &thread) == TRUE) {
				if (thread.th32OwnerProcessID == DWORD(pID_)) {
					++thcnt;
					gatherThread(thread.th32ThreadID);
				}
				// piCout << thread.th32ThreadID;
			}
		}
		tstat.threads = thcnt;
		CloseHandle(snap);
	}
	FILETIME ft0, ft1, ft_kernel, ft_user;
	double el_s = PRIVATE->tm.elapsed_s() * cpu_count / 100.;
	if (GetProcessTimes(PRIVATE->hProc, &ft0, &ft1, &ft_kernel, &ft_user) != 0) {
		PISystemTime tm_kernel_c = FILETIME2PISystemTime(ft_kernel);
		PISystemTime tm_user_c   = FILETIME2PISystemTime(ft_user);
		if (cycle < 0) {
			PRIVATE->tm_kernel = tm_kernel_c;
			PRIVATE->tm_user   = tm_user_c;
		}
		cycle = 0;
		if (el_s <= 0.) {
			tstat.cpu_load_system = 0.f;
			tstat.cpu_load_user   = 0.f;
		} else {
			tstat.cpu_load_system = (tm_kernel_c - PRIVATE->tm_kernel).toSeconds() / el_s;
			tstat.cpu_load_user   = (tm_user_c - PRIVATE->tm_user).toSeconds() / el_s;
		}
		PRIVATE->tm_kernel = tm_kernel_c;
		PRIVATE->tm_user   = tm_user_c;
	} else {
		tstat.cpu_load_system = 0.f;
		tstat.cpu_load_user   = 0.f;
	}
	PRIVATE->tm.reset();
#  endif
#endif

	tstat.cpu_load_system = piClampf(tstat.cpu_load_system, 0.f, 100.f);
	tstat.cpu_load_user   = piClampf(tstat.cpu_load_user, 0.f, 100.f);

	auto i                = cur_tm.makeIterator();
	while (i.next()) {
		if (!last_tm.contains(i.key())) continue;
		ThreadStats & ts_new(i.value());
		ThreadStats & ts_old(last_tm[i.key()]);
		ts_new.cpu_load_kernel = calcThreadUsage(ts_new.kernel_time, ts_old.kernel_time);
		ts_new.cpu_load_user   = calcThreadUsage(ts_new.user_time, ts_old.user_time);
		// piCout << ts_new.cpu_load_user;
	}
	last_tm = cur_tm;
	lock();
	cur_ts = cur_tm.values();
	unlock();
	tstat.ram_total = totalRAM();
	tstat.ram_used  = usedRAM();
	tstat.ram_free  = freeRAM();
	stat_mutex.lock();
	stat = tstat;
	stat.makeStrings();
	stat_mutex.unlock();
	measured();
}


void PISystemMonitor::gatherThread(llong id) {
	PISystemMonitor::ThreadStats ts;
	if (id == 0) return;
	ts.id = id;
#ifdef MICRO_PIP
	ts.name = tbid.value(id, "<PIThread>");
#else
	ts.name = tbid.value(id, "<non-PIThread>");
#  ifndef WINDOWS
	PIFile f(PRIVATE->proc_dir + "task/" + PIString::fromNumber(id) + "/stat");
	// piCout << f.path();
	if (!f.open(PIIODevice::ReadOnly)) return;
	PIString str = PIString::fromAscii(f.readAll());
	int si = str.find('(') + 1, fi = 0, cc = 1;
	for (int i = si; i < str.size_s(); ++i) {
		if (str[i] == '(') cc++;
		if (str[i] == ')') cc--;
		if (cc <= 0) {
			fi = i;
			break;
		}
	}
	str.cutMid(si - 1, fi - si + 3);
	PIStringList sl = str.split(" ");
	if (sl.size_s() < 14) return;
	// piCout << sl[0] << sl[12] << sl[13];
	ts.user_time   = PISystemTime::fromMilliseconds(sl[12].toInt() * 10.);
	ts.kernel_time = PISystemTime::fromMilliseconds(sl[13].toInt() * 10.);
#  else
	PISystemTime ct = PISystemTime::current();
	FILETIME times[4];
	HANDLE thdl = OpenThread(THREAD_QUERY_INFORMATION, FALSE, DWORD(id));
	if (!thdl) {
		piCoutObj << "GatherThread(" << id << "):: OpenThread() error:" << errorString();
		return;
	}
	if (GetThreadTimes(thdl, &(times[0]), &(times[1]), &(times[2]), &(times[3])) == 0) {
		CloseHandle(thdl);
		piCoutObj << "GatherThread(" << id << "):: GetThreadTimes() error:" << errorString();
		return;
	}
	CloseHandle(thdl);
	ts.created     = FILETIME2PIDateTime(times[0]);
	ts.work_time   = ct - ts.created.toSystemTime();
	ts.kernel_time = FILETIME2PISystemTime(times[2]);
	ts.user_time   = FILETIME2PISystemTime(times[3]);
#  endif
#endif
	cur_tm[id] = ts;
}


float PISystemMonitor::calcThreadUsage(PISystemTime & t_new, PISystemTime & t_old) {
	if (delay_.isNull()) return -1.;
	return piClampf(100. * ((t_new - t_old).toMilliseconds() / delay_.toMilliseconds()), 0.f, 100.f);
}


ullong PISystemMonitor::totalRAM() {
#ifdef ESP_PLATFORM
	multi_heap_info_t heap_info;
	piZeroMemory(heap_info);
	heap_caps_get_info(&heap_info, MALLOC_CAP_8BIT);
	return heap_info.total_allocated_bytes + heap_info.total_free_bytes;
#endif
	return 0;
}


ullong PISystemMonitor::freeRAM() {
#ifdef ESP_PLATFORM
	multi_heap_info_t heap_info;
	piZeroMemory(heap_info);
	heap_caps_get_info(&heap_info, MALLOC_CAP_8BIT);
	return heap_info.total_free_bytes;
#endif
	return 0;
}


ullong PISystemMonitor::usedRAM() {
#ifdef ESP_PLATFORM
	multi_heap_info_t heap_info;
	piZeroMemory(heap_info);
	heap_caps_get_info(&heap_info, MALLOC_CAP_8BIT);
	return heap_info.total_allocated_bytes;
#endif
	return 0;
}


PISystemMonitor::Pool * PISystemMonitor::Pool::instance() {
	static Pool ret;
	return &ret;
}


PISystemMonitor * PISystemMonitor::Pool::getByPID(int pID) {
	PIMutexLocker _ml(mutex);
	return sysmons.value(pID, 0);
}


void PISystemMonitor::Pool::add(PISystemMonitor * sm) {
	PIMutexLocker _ml(mutex);
	sysmons[sm->pID()] = sm;
}


void PISystemMonitor::Pool::remove(PISystemMonitor * sm) {
	PIMutexLocker _ml(mutex);
	sysmons.remove(sm->pID());
}
