// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#include "base/unixtime.h"

#include <QDateTime>
#include <QReadWriteLock>

#ifdef Q_OS_WIN
#include <windows.h>
#elif defined Q_OS_MAC
#include <mach/mach_time.h>
#else
#include <time.h>
#endif

namespace base {
namespace unixtime {
namespace {

constexpr auto kIgnoreTimeDifference = TimeId(3);

std::atomic<bool> ValueUpdated/* = false*/;
std::atomic<TimeId> ValueShift/* = 0*/;
std::atomic<bool> HttpValueValid/* = false*/;
std::atomic<TimeId> HttpValueShift/* = 0*/;

class MsgIdManager {
public:
	MsgIdManager();

	void update();
	[[nodiscard]] uint64 next();

private:
	void initialize();

	QReadWriteLock _lock;
	uint64 _startId = 0;
	std::atomic<uint32> _incrementedPart = 0;
	uint64 _startCounter = 0;
	const uint64 _randomPart = 0;
	const float64 _multiplier = 0.;

};

MsgIdManager GlobalMsgIdManager;

[[nodiscard]] float64 GetMultiplier() {
	// 0xFFFF0000 instead of 0x100000000 to make msgId grow slightly slower,
	// than unixtime and we had time to reconfigure.

#ifdef Q_OS_WIN
	LARGE_INTEGER li;
	QueryPerformanceFrequency(&li);
	return float64(0xFFFF0000L) / float64(li.QuadPart);
#elif defined Q_OS_MAC // Q_OS_WIN
	mach_timebase_info_data_t tb = { 0, 0 };
	mach_timebase_info(&tb);
	const auto frequency = (float64(tb.numer) / tb.denom) / 1000000.;
	return frequency * (float64(0xFFFF0000L) / 1000.);
#else // Q_OS_MAC || Q_OS_WIN
	return float64(0xFFFF0000L) / 1000000000.;
#endif // Q_OS_MAC || Q_OS_WIN
}

[[nodiscard]] uint64 GetCounter() {
#ifdef Q_OS_WIN
	LARGE_INTEGER li;
	QueryPerformanceCounter(&li);
	return li.QuadPart;
#elif defined Q_OS_MAC // Q_OS_WIN
	return mach_absolute_time();
#else // Q_OS_MAC || Q_OS_WIN
	timespec ts;
	clock_gettime(CLOCK_MONOTONIC, &ts);
	return 1000000000 * uint64(ts.tv_sec) + uint64(ts.tv_nsec);
#endif // Q_OS_MAC || Q_OS_WIN
}

[[nodiscard]] uint32 PrepareRandomPart() {
	auto generator = std::mt19937(std::random_device()());
	auto distribution = std::uniform_int_distribution<uint32>();
	return distribution(generator);
}

MsgIdManager::MsgIdManager()
: _randomPart(PrepareRandomPart())
, _multiplier(GetMultiplier()) {
	initialize();

	srand(uint32(_startCounter & 0xFFFFFFFFUL));
}

void MsgIdManager::update() {
	QWriteLocker lock(&_lock);
	initialize();
}

void MsgIdManager::initialize() {
	_startCounter = GetCounter();
	_startId = ((uint64(uint32(now()))) << 32) | _randomPart;
}

uint64 MsgIdManager::next() {
	const auto counter = GetCounter();

	QReadLocker lock(&_lock);
	const auto startCounter = _startCounter;
	const auto startId = _startId;
	lock.unlock();

	const auto incrementedPart = (_incrementedPart += 4);

	const auto delta = (counter - startCounter);
	const auto result = startId + (uint64)floor(delta * _multiplier);

	return (result & ~0x03L) + incrementedPart;
}

TimeId local() {
	return (TimeId)time(nullptr);
}

rpl::event_stream<> &UpdatesStream() {
	static auto result = rpl::event_stream<>();
	return result;
}

} // namespace

TimeId now() {
	return local() + ValueShift.load();
}

void update(TimeId now, bool force) {
	if (force) {
		ValueUpdated = true;
	} else if (ValueUpdated) {
		return;
	}

	const auto shift = now - local();
	const auto ignore = !force
		&& (std::abs(shift - ValueShift) < kIgnoreTimeDifference);
	if (ignore) {
		return;
	} else if (!force) {
		auto expected = false;
		if (!ValueUpdated.compare_exchange_strong(expected, true)) {
			return;
		}
	}
	const auto old = ValueShift.exchange(shift);

	HttpValueShift = 0;
	HttpValueValid = false;

	if (old != shift) {
		GlobalMsgIdManager.update();

		crl::on_main([] {
			UpdatesStream().fire({});
		});
	}
}

rpl::producer<> updates() {
	return UpdatesStream().events();
}

QDateTime parse(TimeId value) {
	return (value > 0)
		? QDateTime::fromSecsSinceEpoch(value - ValueShift)
		: QDateTime();
}

TimeId serialize(const QDateTime &date) {
	return date.isNull() ? TimeId(0) : date.toSecsSinceEpoch() + ValueShift;
}

bool http_valid() {
	return HttpValueValid;
}

TimeId http_now() {
	return now() + HttpValueShift;
}

void http_update(TimeId now) {
	HttpValueShift = now - base::unixtime::now();
	HttpValueValid = true;
}

void http_invalidate() {
	HttpValueValid = false;
}

uint64 mtproto_msg_id() {
	return GlobalMsgIdManager.next();
}

} // namespace unixtime
} // namespace base
