#include <config.h>
#include <iostream>
#include <chrono>
#include <algorithm>
#include <boost/asio/io_context.hpp>
#include <opts.h>
#include <componentThread.h>
#include <adapters/boostAsio/deadlineTimerAReq.h>
#include <user/stimulusProducer.h>
#include <user/stimulusBuffer.h>

namespace smo {
namespace stim_buff {

namespace {

long computeTimesliceResidueMs(
	long productionDurationMs, long periodMs)
{
	if (productionDurationMs >= periodMs) {
		return 0;
	}
	return periodMs - productionDurationMs;
}

void logProductionOverrunIfNeeded(
	const char *daemonName,
	long productionDurationMs, long periodMs,
	size_t &nTimesliceOverruns)
{
	if (productionDurationMs <= periodMs) {
		return;
	}

	++nTimesliceOverruns;
	const long overrunByMs = productionDurationMs - periodMs;
	std::cerr << daemonName << ": production overrun: actual="
		<< productionDurationMs << "ms budget=" << periodMs
		<< "ms overrunBy=" << overrunByMs << "ms nOverruns="
		<< nTimesliceOverruns << std::endl;
}

long durationMsSince(
	const std::chrono::high_resolution_clock::time_point &startStamp,
	const std::chrono::high_resolution_clock::time_point &endStamp)
{
	const auto duration = endStamp - startStamp;
	return std::chrono::duration_cast<std::chrono::milliseconds>(
		duration).count();
}

void logDaemonDurationsIfVerbose(
	const char *daemonName,
	long productionDurationMs,
	long timesliceDurationMs,
	long periodMs)
{
	if (!OptionParser::getOptions().verbose) {
		return;
	}

	std::cerr << daemonName << ": daemon durations: production="
		<< productionDurationMs << "ms timeslice="
		<< timesliceDurationMs << "ms period=" << periodMs
		<< "ms" << std::endl;
}

} // namespace

std::shared_ptr<StimulusBuffer> StimulusProducer::getAttachedStimulusBuffer(
	const std::shared_ptr<device::DeviceAttachmentSpec>& spec) const
{
	for (const auto& buffer : attachedStimulusBuffers)
	{
		if (buffer && buffer->deviceAttachmentSpec &&
			*buffer->deviceAttachmentSpec == *spec)
		{
			return buffer;
		}
	}

	return nullptr;
}

std::shared_ptr<StimulusBuffer> StimulusProducer::getAttachedStimulusBufferByAttachIdentity(
	const std::string& deviceIdentifier,
	const std::string& qualeIfaceApi) const
{
	for (const auto& buffer : attachedStimulusBuffers)
	{
		if (!buffer || !buffer->deviceAttachmentSpec)
		{
			throw std::runtime_error(
				"StimulusProducer::getAttachedStimulusBufferByAttachIdentity: "
				"encountered null buffer or null deviceAttachmentSpec in "
				"attachedStimulusBuffers (should never happen)");
		}

		if (buffer->deviceAttachmentSpec->deviceIdentifier != deviceIdentifier)
			{ continue; }

		if (buffer->deviceAttachmentSpec->qualeIfaceApi != qualeIfaceApi)
			{ continue; }

		return buffer;
	}

	return nullptr;
}

bool StimulusProducer::hasBufferWithQualeIfaceApi(
	const std::string& qualeIfaceApi) const
{
	for (const auto& buffer : attachedStimulusBuffers)
	{
		if (!buffer || !buffer->deviceAttachmentSpec)
		{
			throw std::runtime_error(
				"StimulusProducer::hasBufferWithQualeIfaceApi: encountered "
				"null buffer or null deviceAttachmentSpec in "
				"attachedStimulusBuffers (should never happen)");
		}

		if (buffer->deviceAttachmentSpec->qualeIfaceApi != qualeIfaceApi)
			{ continue; }

		return true;
	}

	return false;
}

void StimulusProducer::ensureNoDuplicateQualeIface(
	const std::string& qualeIfaceApi) const
{
	if (!hasBufferWithQualeIfaceApi(qualeIfaceApi)) {
		return;
	}

	throw std::runtime_error(
		"duplicate qualeIface '" + qualeIfaceApi
		+ "' for this producer session");
}

bool StimulusProducer::addAttachedStimulusBufferIfNotExists(
	const std::shared_ptr<StimulusBuffer>& buffer)
{
	if (!buffer) { return false; }

	auto it = std::find_if(
		attachedStimulusBuffers.begin(),
		attachedStimulusBuffers.end(),
		[&](const std::shared_ptr<StimulusBuffer>& buf) {
			return buf && buffer &&
				buf->deviceAttachmentSpec && buffer->deviceAttachmentSpec &&
				*(buf->deviceAttachmentSpec) == *(buffer->deviceAttachmentSpec);
		});

	if (it != attachedStimulusBuffers.end()) {
		return false;
	}

	attachedStimulusBuffers.push_back(buffer);
	return true;
}

void StimulusProducer::destroyAttachedStimulusBuffer(
	const std::shared_ptr<StimulusBuffer>& buffer)
{
	if (!buffer) { return; }

	auto it = std::find(
		attachedStimulusBuffers.begin(),
		attachedStimulusBuffers.end(),
		buffer);

	if (it != attachedStimulusBuffers.end()) {
		attachedStimulusBuffers.erase(it);
	}
}

sscl::co::NonViralNonPostingInvoker StimulusProducer::productionCDaemon(
	std::exception_ptr &, std::function<void()>,
	sscl::SyncCancelerForAsyncWork &canceler)
{
	const long framePeriodMs = CONFIG_STIMBUFF_FRAME_PERIOD_MS;

	do
	{
		if (canceler.isCancellationRequested()) {
			break;
		}

		const auto timesliceStartStamp =
			std::chrono::high_resolution_clock::now();

		const auto productionStartStamp =
			std::chrono::high_resolution_clock::now();

		co_await stimFrameProductionTimesliceCInd(canceler);

		const auto productionEndStamp =
			std::chrono::high_resolution_clock::now();
		const long productionDurationMs = durationMsSince(
			productionStartStamp, productionEndStamp);

		logProductionOverrunIfNeeded(
			"productionCDaemon",
			productionDurationMs, framePeriodMs, nTimesliceOverruns);

		const long residueMs = computeTimesliceResidueMs(
			productionDurationMs, framePeriodMs);

		// Schedule the next timeout based on timeslice remaining time.
		const bool expiredNormally = co_await
			adapters::boostAsio::getDeadlineTimerAReqAwaiter(
				ioContext,
				daemonTimer,
				boost::posix_time::milliseconds(residueMs));

		if (!expiredNormally) {
			// Timer was cancelled, which is expected when stopping
			break;
		}

		const auto timesliceEndStamp =
			std::chrono::high_resolution_clock::now();
		const long timesliceDurationMs = durationMsSince(
			timesliceStartStamp, timesliceEndStamp);

		logDaemonDurationsIfVerbose(
			"productionCDaemon",
			productionDurationMs, timesliceDurationMs,
			framePeriodMs);

	} while (!canceler.isCancellationRequested());

	co_return;
}

void StimulusProducer::start()
{
	std::cout << __func__ << ": Starting stimulus producer for device "
		<< deviceAttachmentSpec->deviceSelector << std::endl;

	nTimesliceOverruns = 0;
	taskNursery.openAdmission();
	taskNursery.launch(
		[this](sscl::co::NonViralTaskNursery::Slot::Lease &lease)
		{
			return productionCDaemon(
				lease.getExceptionStorage(),
				lease.getCallerLambda(),
				lease.getSyncCanceler());
		});
}

void StimulusProducer::stop()
{
	// Cancel timer immediately
	daemonTimer.cancel();
	taskNursery.requestCancelOnAll();
	taskNursery.closeAdmission();
	taskNursery.syncAwaitAllSettlements(
		sscl::ComponentThread::getSelf()->getIoContext());

	std::cout << __func__ << ": Stopped stimulus producer for device "
		<< deviceAttachmentSpec->deviceSelector << std::endl;
}

} // namespace stim_buff
} // namespace smo
