#ifndef BROADCAST_LISTENER_H
#define BROADCAST_LISTENER_H

#include <boostAsioLinkageFix.h>

#include <array>
#include <vector>
#include <string>
#include <memory>
#include <functional>
#include <boost/asio/ip/udp.hpp>
#include <user/senseApiDesc.h>
#include <spinscale/spinLock.h>
#include <spinscale/sharedResourceGroup.h>
#include <spinscale/co/dynamicPostingInvoker.h>
#include <spinscale/co/nonViralTaskNursery.h>
#include <spinscale/syncCancelerForAsyncWork.h>
#include "device.h"

namespace livoxProto1 {
namespace comms {

/**		EXPLANATION:
 * This class merely listens for UDP bcast dgrams on the designated listening
 * port. It then builds a list of client device IP addrs that it has heard from.
 * It doesn't connect to them or signal any events to the rest of the lib,
 * except in the case that a device which the lib is using has gone away.
 *
 * Other than that, its role is to tell the lib which devices are available
 * on the network.
 */
#define UDP_BCAST_MSG_BUFFER_NBYTES		(1024)

class BroadcastListener
{
public:
	BroadcastListener(
		const std::shared_ptr<sscl::ComponentThread>& componentThread,
		uint16_t listeningPort=55000, uint16_t connectPort=65000);

	~BroadcastListener() = default;

	typedef void (DeviceGoneAwayCbFn)(const DiscoveredDevice &device);
	void setDeviceGoneAwayCb(DeviceGoneAwayCbFn *cb)
		{ deviceGoneAwayCb = cb; }

	bool deviceExists(const std::string &deviceIdentifier) const
		{ return getDevice(deviceIdentifier) != nullptr; }

	std::shared_ptr<DiscoveredDevice>
	getDevice(const std::string &deviceIdentifier) const;

	void start(void);
	void stop(void);

private:
	/** Per-datagram snapshot; bytes are copied in ctor/move so handlers remain
	 * safe if broadcastMsgIndCInd is ever posted asynchronously.
	 */
	struct BroadcastIndPayload
	{
		std::array<uint8_t, UDP_BCAST_MSG_BUFFER_NBYTES> bytes{};
		std::size_t nbytes = 0;
		boost::asio::ip::udp::endpoint senderEndpoint;

		BroadcastIndPayload() = default;

		BroadcastIndPayload(
			const uint8_t *receiveBytes, std::size_t receiveNbytes,
			const boost::asio::ip::udp::endpoint &endpoint);

		BroadcastIndPayload(BroadcastIndPayload &&other) noexcept;

		BroadcastIndPayload(const BroadcastIndPayload &) = delete;
		BroadcastIndPayload &operator=(const BroadcastIndPayload &) = delete;
		BroadcastIndPayload &operator=(BroadcastIndPayload &&) = delete;
	};

	/**	EXPLANATION:
	 * broadcastReceiveCDaemon is a dynamic posting non-viral coroutine: start()
	 * passes ExplicitPostTarget{componentThread->getIoContext()} so the daemon
	 * body always runs on componentThread. Synchronous listener state is
	 * touched only inside canceler.execUncancelableSegmentOrAbort() so stop()
	 * cannot tear down *this while the daemon is in a critical section.
	 */
	sscl::co::DynamicNonViralPostingInvoker broadcastReceiveCDaemon(
		sscl::co::ExplicitPostTarget postTarget,
		std::exception_ptr &exceptionPtr,
		std::function<void()> callback,
		sscl::SyncCancelerForAsyncWork &canceler);

	sscl::co::NonViralNonPostingInvoker broadcastMsgIndCInd(
		std::exception_ptr &exceptionPtr,
		std::function<void()> callback,
		sscl::SyncCancelerForAsyncWork &canceler,
		BroadcastIndPayload payload);

	void registerDiscoveredDevice(
		const BroadcastMessage &msg, const std::string &senderIp);

private:
	std::shared_ptr<sscl::ComponentThread> componentThread;
	/**		EXPLANATION:
	 * The Livox proto says that client devices will spam broadcast UDP
	 * dgrams to us on the listening port. We can then use the source IP from
	 * the bcast dgram to figure out the client device's IP addr. Then we
	 * should send a connect dgram to the connect port. This will tell the
	 * client device our IP addr.
	 */
	uint16_t listeningPort, connectPort;
	DeviceGoneAwayCbFn *deviceGoneAwayCb;

	struct DiscoveredDevicesResources
	{
		std::vector<std::shared_ptr<DiscoveredDevice>> devices;
	};

	mutable sscl::SharedResourceGroup<
		sscl::SpinLock, DiscoveredDevicesResources> discoveredDevices;

	boost::asio::ip::udp::socket socket;
	boost::asio::ip::udp::endpoint listeningEndpoint;

	/** Hosts broadcastReceiveCDaemon; stop() cancels and syncAwaitAll's it. */
	sscl::co::NonViralTaskNursery daemonNursery;
	/** Hosts per-datagram broadcastMsgIndCInd; admission/drain RAII inside CDaemon. */
	sscl::co::NonViralTaskNursery eventHandlerNursery;
};

} // namespace comms
} // namespace livoxProto1

#endif // BROADCAST_LISTENER_H
