/*
    PIP - Platform Independent Primitives
    Unix time
    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 "pisystemtime.h"

#include "piincludes_p.h"
#include "piiostream.h"
#include "piliterals_time.h"
#include "pitime.h"
#include "pitranslator.h"

#include <ctime>
#ifdef QNX
#  include <time.h>
#endif
#ifndef MICRO_PIP
#  include "pisystemtests.h"
#elif defined(ARDUINO)
#  include <Arduino.h>
#endif
#ifdef WINDOWS
extern FILETIME __pi_ftjan1970;
long long __PIQueryPerformanceCounter() {
	LARGE_INTEGER li;
	QueryPerformanceCounter(&li);
	return li.QuadPart;
}
#endif
#ifdef MAC_OS
// #  include <mach/mach_traps.h>
// #  include <mach/mach.h>
#  include <mach/clock.h>
// #  include <crt_externs.h>
extern clock_serv_t __pi_mac_clock;
#endif
#ifdef MICRO_PIP
#  include <sys/time.h>
#endif


//! \class PISystemTime pisystemtime.h
//! \details
//! \~english \section PISystemTime_sec0 Synopsis
//! \~russian \section PISystemTime_sec0 Краткий обзор
//! \~english
//! This class provide arithmetic functions for POSIX system time.
//! This time represents as seconds and nanosecons in integer formats.
//! You can take current system time with function \a PISystemTime::current(),
//! compare times, sum or subtract two times, convert time to/from
//! seconds, milliseconds, microseconds or nanoseconds.
//!
//! \~russian
//! Этот класс предоставляет арифметику для системного времени в формате POSIX.
//! Это время представлено в виде целочисленных секунд и наносекунд.
//! Можно взять текущее время с помощью метода \a PISystemTime::current(),
//! сравнивать, суммировать и вычитать времена, преобразовывать в/из
//! секунд, миллисекунд, микросекунд и наносекунд.
//!
//! \~english \section PISystemTime_sec1 Example
//! \~russian \section PISystemTime_sec1 Пример
//! \~\snippet pitimer.cpp system_time
//!


//! \class PITimeMeasurer pisystemtime.h
//! \details
//! \~english \section PITimeMeasurer_sec0 Usage
//! \~russian \section PITimeMeasurer_sec0 Использование
//! \~english
//! Function \a reset() set time mark to current
//! system time, then functions "double elapsed_*()" returns time elapsed from this mark.
//! These functions can returns nano-, micro-, milli- and seconds with suffixes "n", "u", "m"
//! and "s"
//!
//! \~russian
//! Метод \a reset() устанавливает текущую метку системного времени. Далее методы
//! "double elapsed_*()" возвращают время, прошедшее от установленной метки.
//! Эти методы возвращают нано, микро, милли и секунды с приставками
//! "n", "u", "m" и "s".
//!


//! \details
//! \warning
//! \~english
//! Use this function to sleep for difference of system times or constructs system time.
//! If you call this function on system time returned with \a PISystemTime::current() thread will be sleep almost forever
//! \~russian
//! Используйте этот метод для ожидания разниц системных времен или своего времени.
//! Если метод будет вызван для системного времени \a PISystemTime::current(), то
//! ожидание будет почти бесконечным
void PISystemTime::sleep() const {
	piUSleep(piFloord(toMicroseconds()));
}


void PISystemTime::toTimespec(void * ts) {
#ifndef WINDOWS
	((timespec *)ts)->tv_sec  = seconds;
	((timespec *)ts)->tv_nsec = nanoseconds;
#endif
}


PISystemTime::Frequency PISystemTime::toFrequency() const {
	return PISystemTime::Frequency::fromSystemTime(*this);
}


PIString PISystemTime::toString() const {
	static const PISystemTime is_abs_thr = 3650._d; // 10 years
	if ((*this) >= is_abs_thr) return PIDateTime::fromSystemTime((*this)).toString("yyyy-MM-dd hh:mm:ss.zzz");
	auto extract = [](int & secs, int part) {
		int p = secs / part;
		secs -= p * part;
		return p;
	};
	int s  = seconds;
	int d  = extract(s, 60 * 60 * 24);
	int h  = extract(s, 60 * 60);
	int m  = extract(s, 60);
	int ns = nanoseconds;
	int ms = extract(ns, 1000000);
	int us = extract(ns, 1000);

	PIString ret;
	PIIOTextStream ts(&ret, PIIODevice::WriteOnly);
	auto print = [&ts, &ret](int part, const char * name) {
		if (part > 0) {
			if (ret.isNotEmpty()) ts << ' ';
			ts << part << ' ' << name;
		}
	};
	print(d, "d");
	print(h, "h");
	print(m, "m");
	print(s, "s");
	print(ms, "ms");
	print(us, "us");
	print(ns, "ns");
	return ret;
}


PISystemTime PISystemTime::abs() const {
	if (seconds < 0)
		return PISystemTime(piAbsl(seconds) - 1, 1000000000l - piAbsl(nanoseconds));
	else
		return PISystemTime(piAbsl(seconds), piAbsl(nanoseconds));
}


PISystemTime PISystemTime::fromString(PIString s) {
	if (s.findAny("-:") >= 0) {
		return PIDateTime::fromString(s, "y-M-d h:m:s.z").toSystemTime();
	}
	// clang-format off
	static const PIMap<PIString, PISystemTime> times({
		{"d", 1_d}, {"day", 1_d}, {"days", 1_d},
		{"h", 1_h}, {"hour", 1_h}, {"hours", 1_h},
		{"m", 1_m}, {"min", 1_m}, {"mins", 1_m}, {"minute", 1_m}, {"minutes", 1_m},
		{"s", 1_s}, {"sec", 1_s}, {"secs", 1_s}, {"second", 1_s}, {"seconds", 1_s},
		{"ms", 1_ms}, {"msec", 1_ms}, {"msecs", 1_ms}, {"millisecond", 1_ms}, {"milliseconds", 1_ms}, {"milli", 1_ms}, {"millis", 1_ms},
		{"us", 1_us}, {"usec", 1_us}, {"usecs", 1_us}, {"microsecond", 1_us}, {"microseconds", 1_us}, {"micro", 1_us}, {"micros", 1_us},
		{"ns", 1_ns}, {"nsec", 1_ns}, {"nsecs", 1_ns}, {"nanosecond", 1_ns}, {"nanoseconds", 1_ns}, {"nano", 1_ns}, {"nanos", 1_ns}
	});
	static const PIMap<PIString, PISystemTime::Frequency> freqs({
		{"hz", 1_Hz},
		{"khz", 1_KHz},
		{"mhz", 1_MHz},
		{"ghz", 1_GHz}
	});
	static const PIString num_syms = PIStringAscii("0123456789.eE+-");
	// clang-format on
	s = s.toLowerCase();
	PISystemTime sum_t;
	PISystemTime::Frequency sum_f;
	auto extract = [&sum_t, &sum_f](PIString & str) {
		PIString vn, pn;
		str.trim();
		bool found = false;
		for (int i = 0; i < str.size_s(); ++i)
			if (!num_syms.contains(str[i])) {
				vn    = str.takeLeft(i);
				found = true;
				break;
			}
		if (!found) return false;
		pn = str.takeWord();
		if (pn.isEmpty()) return false;
		if (times.contains(pn))
			sum_t += times.value(pn) * vn.toDouble();
		else if (freqs.contains(pn))
			sum_f += freqs.value(pn) * vn.toDouble();
		return true;
	};
	while (extract(s))
		;
	if (!sum_t.isNull() && !sum_f.isNull()) piCout << "[PISystemTime] fromString() warning: ambiguous string, time and frequency!";
	if (sum_t.isNull() && !sum_f.isNull()) sum_t = sum_f.toSystemTime();
	return sum_t;
}


PISystemTime PISystemTime::current(bool precise_but_not_system) {
#ifdef WINDOWS
	if (precise_but_not_system) {
		llong qpc(0);
		if (__pi_perf_freq > 0) {
			qpc = __PIQueryPerformanceCounter();
			return PISystemTime::fromSeconds(qpc / double(__pi_perf_freq));
		}
		return PISystemTime();
	} else {
		FILETIME ft, sft;
#  if (_WIN32_WINNT >= 0x0602)
		GetSystemTimePreciseAsFileTime(&ft);
#  else
		GetSystemTimeAsFileTime(&ft);
#  endif
		sft.dwHighDateTime = ft.dwHighDateTime - __pi_ftjan1970.dwHighDateTime;
		if (ft.dwLowDateTime < __pi_ftjan1970.dwLowDateTime) {
			sft.dwLowDateTime = ft.dwLowDateTime + (0xFFFFFFFF - __pi_ftjan1970.dwLowDateTime);
			sft.dwHighDateTime--;
		} else
			sft.dwLowDateTime = ft.dwLowDateTime - __pi_ftjan1970.dwLowDateTime;
		ullong lt = ullong(sft.dwHighDateTime) * 0x100000000U + ullong(sft.dwLowDateTime);
		return PISystemTime(lt / 10000000U, (lt % 10000000U) * 100U);
	}
#elif defined(MAC_OS)
	mach_timespec_t t_cur;
	clock_get_time(__pi_mac_clock, &t_cur);
#elif defined(MICRO_PIP)
	timespec t_cur;
#  ifdef ARDUINO
	static const uint32_t offSetSinceEpoch_s = 1581897605UL;
	uint32_t mt                              = millis();
	t_cur.tv_sec                             = offSetSinceEpoch_s + (mt / 1000);
	t_cur.tv_nsec                            = (mt - (mt / 1000)) * 1000000UL;
#  else
	timeval tv;
	tv.tv_sec  = 0;
	tv.tv_usec = 0;
	gettimeofday(&tv, NULL);
	t_cur.tv_sec  = tv.tv_sec;
	t_cur.tv_nsec = tv.tv_usec * 1000;
#  endif
#else
	timespec t_cur;
	clock_gettime(precise_but_not_system ? CLOCK_MONOTONIC : 0, &t_cur);
#endif
#ifndef WINDOWS
	return PISystemTime(t_cur.tv_sec, t_cur.tv_nsec);
#endif
}


PITimeMeasurer::PITimeMeasurer() {
	reset();
}


double PITimeMeasurer::elapsed_n() const {
	return (PISystemTime::current(true) - t_st).toNanoseconds()
#ifndef MICRO_PIP
	       - PISystemTests::time_elapsed_ns
#endif
		;
}


double PITimeMeasurer::elapsed_u() const {
	return (PISystemTime::current(true) - t_st).toMicroseconds()
#ifndef MICRO_PIP
	       - PISystemTests::time_elapsed_ns / 1.E+3
#endif
		;
}


double PITimeMeasurer::elapsed_m() const {
	return (PISystemTime::current(true) - t_st).toMilliseconds()
#ifndef MICRO_PIP
	       - PISystemTests::time_elapsed_ns / 1.E+6
#endif
		;
}


double PITimeMeasurer::elapsed_s() const {
	return (PISystemTime::current(true) - t_st).toSeconds()
#ifndef MICRO_PIP
	       - PISystemTests::time_elapsed_ns / 1.E+9
#endif
		;
}


PISystemTime PITimeMeasurer::elapsed() const {
	return (PISystemTime::current(true) - t_st);
}


PISystemTime PITimeMeasurer::elapsedAndReset() {
	auto ct  = PISystemTime::current(true);
	auto ret = ct - t_st;
	t_st     = ct;
	return ret;
}


// PISystemTime::Frequency

PISystemTime PISystemTime::Frequency::toSystemTime() const {
	if (value_hz <= 0.) {
		piCout << "[PISystemTime::Frequency]"
			   << "toSystemTime() Warning: invalid hertz: %1"_tr("PISystemTime").arg(value_hz);
		return PISystemTime();
	}
	return PISystemTime::fromSeconds(1. / value_hz);
}


PISystemTime::Frequency PISystemTime::Frequency::fromSystemTime(const PISystemTime & st) {
	if (st == PISystemTime()) {
		piCout << "[PISystemTime::Frequency]"
			   << "fromSystemTime() Warning: null frequency"_tr("PISystemTime");
		return Frequency();
	}
	return Frequency(1. / st.toSeconds());
}
