// SPDX-License-Identifier: LGPL-3.0-or-later
// Copyright © 2020-2021 Florian Schmaus
#include <atomic>		 // for atomic_thread_fence, memory_...
#include <cstdint>	 // for uint64_t
#include <cstdlib>	 // for exit, EXIT_FAILURE, EXIT_SUC...
#include <iostream>	 // for operator<<, basic_ostream

#include "Actor.hpp"										 // for Actor
#include "CountingPrivateSemaphore.hpp"	 // for CPS
#include "Debug.hpp"										 // for DBG
#include "Dispatcher.hpp"								 // for Dispatcher
#include "Fiber.hpp"										 // for Fiber
#include "Runtime.hpp"									 // for Runtime
#include "emper-common.h"								 // for UNUSED_ARG
#include "emper-config.h"								 // // IWYU pragma: keep
#include "emper.hpp"										 // for spawn

class SumActor : public Actor<uint64_t> {
 private:
	uint64_t sum = 0;

 protected:
	void receive(uint64_t t) override { sum += t; }

 public:
	SumActor(Runtime& runtime) : Actor(runtime) {}

	[[nodiscard]] auto getSum() const -> uint64_t {
		std::atomic_thread_fence(std::memory_order_acquire);
		return sum;
	}

	void stop() { Actor::stop(); }
};

static void mainFiber(void* runtime_ptr) {
	Runtime& runtime = *(Runtime*)runtime_ptr;
	const unsigned int FIBER_COUNT = EMPER_LOG_LEVEL > Info ? 500 : 2500;
	const uint64_t FIBERS_COUNT_TO = 1000;
	const uint64_t PER_FIBER_SUM = (FIBERS_COUNT_TO * (FIBERS_COUNT_TO + 1)) / 2;
	const uint64_t EXPECTED_SUM = FIBER_COUNT * PER_FIBER_SUM;

	SumActor sumActor(runtime);
	sumActor.start();

	CPS cps;
	for (unsigned int fiberNum = 0; fiberNum < FIBER_COUNT; ++fiberNum) {
		spawn(
				[&sumActor, fiberNum] {
					DBG(Dispatcher::getCurrentFiber()
							<< " (" << fiberNum << ") starts to count to " << FIBERS_COUNT_TO);
					for (uint64_t i = 1; i <= FIBERS_COUNT_TO; ++i) {
						sumActor.tell(i);
					}
				},
				cps);
	}

	// Wait for the producer fibers to finish.
	cps.wait();

	// Wait for the actor to become idle.
	bool actorIdle = sumActor.waitUntilIdle(60);
	if (!actorIdle) {
		std::cerr << "FAILURE: Actor did not went idle";
		exit(EXIT_FAILURE);
	}

	sumActor.stop();

	if (sumActor.getSum() != EXPECTED_SUM) {
		std::cerr << "FAILURE: Actor sum " << sumActor.getSum() << " is not equal to expected sum "
							<< EXPECTED_SUM << std::endl;
		exit(EXIT_FAILURE);
	}

	exit(EXIT_SUCCESS);
}

auto main(UNUSED_ARG int arg, UNUSED_ARG char* argv[]) -> int {
	Runtime runtime;

	Fiber* fiber = Fiber::from(mainFiber, (void*)&runtime);
	runtime.scheduleFromAnywhere(*fiber);

	runtime.waitUntilFinished();

	return EXIT_FAILURE;
}