/* quick-start.cpp - the CAS BACnet Stack Quick Start device, assembled from the chapter's code.
   Build it with udp.cpp, the socket layer. https://www.bacnetstack.com/documentation/getting-started/quick-start/ */
#include "CASBACnetStackAdapter.h"
#include <stdio.h>
#include <string.h>
#include <time.h>
#if defined(_WIN32)
#include <windows.h>
#else
#include <unistd.h>
#endif

static const uint32_t DEVICE_INSTANCE = 389001;
static const uint32_t NETWORK_PORT_INSTANCE = 1;
static const uint32_t ANALOG_INPUT_INSTANCE = 1;

/* BACnet enumeration values. The API Reference's Enumerations section lists them all. */
static const uint16_t OBJECT_TYPE_ANALOG_INPUT = 0;
static const uint16_t OBJECT_TYPE_DEVICE = 8;
static const uint16_t OBJECT_TYPE_NETWORK_PORT = 56;
static const uint32_t PROPERTY_MODEL_NAME = 70;
static const uint32_t PROPERTY_OBJECT_NAME = 77;
static const uint32_t PROPERTY_PRESENT_VALUE = 85;
static const uint32_t PROPERTY_UNITS = 117;
static const uint32_t PROPERTY_VENDOR_IDENTIFIER = 120;
static const uint32_t PROPERTY_VENDOR_NAME = 121;
static const uint32_t PROPERTY_IP_ADDRESS = 400;
static const uint32_t PROPERTY_IP_DEFAULT_GATEWAY = 401;
static const uint32_t PROPERTY_IP_SUBNET_MASK = 411;
static const uint32_t PROPERTY_BACNET_IP_UDP_PORT = 412;
static const uint32_t UNITS_DEGREES_CELSIUS = 62;

/* The host's BACnet/IP settings, filled in at startup. */
static uint8_t g_ipAddress[4];
static uint8_t g_ipSubnetMask[4];
static uint8_t g_ipDefaultGateway[4];
static uint16_t g_udpPort = 47808;

/* The application's data: one temperature sensor. */
static float g_roomTemperature = 21.5f;

/* Your socket layer, not part of the stack. Neither function may block. */
bool UdpOpen(uint16_t port, uint8_t ipAddress[4], uint8_t subnetMask[4]);
uint16_t UdpReceive(uint8_t* buffer, uint16_t bufferLength, uint8_t fromAddress[4], uint16_t* fromPort);
uint16_t UdpSend(const uint8_t toAddress[4], uint16_t toPort, const uint8_t* data, uint16_t length);

uint16_t ReceiveMessage(uint8_t* message, const uint16_t maxMessageLength,
	uint8_t* sourceConnectionString, uint8_t* sourceConnectionStringLength,
	uint8_t* destinationConnectionString, uint8_t* destinationConnectionStringLength,
	const uint8_t maxConnectionStringLength, uint32_t* networkPortInstance)
{
	(void)destinationConnectionString;
	if (maxConnectionStringLength < 6) {
		return 0;
	}

	uint8_t fromAddress[4];
	uint16_t fromPort = 0;
	const uint16_t bytesRead = UdpReceive(message, maxMessageLength, fromAddress, &fromPort);
	if (bytesRead == 0) {
		return 0; /* Nothing waiting. */
	}

	memcpy(sourceConnectionString, fromAddress, 4);
	sourceConnectionString[4] = (uint8_t)(fromPort >> 8);
	sourceConnectionString[5] = (uint8_t)(fromPort & 0xFF);
	*sourceConnectionStringLength = 6;
	*destinationConnectionStringLength = 0; /* Fill it in if your socket API reports the destination. */
	*networkPortInstance = NETWORK_PORT_INSTANCE;
	return bytesRead;
}

uint16_t SendMessage(const uint8_t* message, const uint16_t messageLength,
	const uint8_t* connectionString, const uint8_t connectionStringLength,
	const uint32_t networkPortInstance, bool broadcast)
{
	if (networkPortInstance != NETWORK_PORT_INSTANCE) {
		return 0; /* Not a port this program owns. */
	}

	uint8_t toAddress[4];
	uint16_t toPort = g_udpPort;
	if (connectionStringLength == 6) {
		/* Send to the address given: a device, or a broadcast address this program asked for. */
		memcpy(toAddress, connectionString, 4);
		toPort = (uint16_t)((connectionString[4] << 8) | connectionString[5]);
	} else if (broadcast) {
		/* A broadcast the stack chose itself: send it to this subnet's broadcast address. */
		for (int octetIndex = 0; octetIndex < 4; octetIndex++) {
			toAddress[octetIndex] = (uint8_t)(g_ipAddress[octetIndex] | ~g_ipSubnetMask[octetIndex]);
		}
	} else {
		return 0;
	}

	const uint16_t bytesSent = UdpSend(toAddress, toPort, message, messageLength);
	return bytesSent == messageLength ? messageLength : 0;
}

CASBACnetTime GetSystemTime()
{
	return (CASBACnetTime)time(NULL);
}

void LogDebugMessage(const char* message, const uint16_t messageLength, const uint8_t messageType)
{
	fprintf(messageType == 0 ? stderr : stdout, "%.*s", (int)messageLength, message);
}

static bool ReturnText(const char* text, char* value, uint32_t* valueElementCount,
	const uint32_t maxElementCount, uint8_t* encodingType)
{
	uint32_t textLength = (uint32_t)strlen(text);
	if (textLength > maxElementCount) {
		textLength = maxElementCount;
	}
	memcpy(value, text, textLength);
	*valueElementCount = textLength;
	*encodingType = 0; /* UTF-8 */
	return true;
}

bool GetPropertyCharacterString(const uint32_t deviceInstance, const uint16_t objectType,
	const uint32_t objectInstance, const uint32_t propertyIdentifier, char* value,
	uint32_t* valueElementCount, const uint32_t maxElementCount, uint8_t* encodingType,
	const bool useArrayIndex, const uint32_t propertyArrayIndex, uint32_t* errorCode)
{
	(void)deviceInstance; (void)useArrayIndex; (void)propertyArrayIndex; (void)errorCode;
	if (objectType == OBJECT_TYPE_DEVICE) {
		switch (propertyIdentifier) {
			case PROPERTY_OBJECT_NAME: return ReturnText("My First Device", value, valueElementCount, maxElementCount, encodingType);
			case PROPERTY_VENDOR_NAME: return ReturnText("Example Company", value, valueElementCount, maxElementCount, encodingType);
			case PROPERTY_MODEL_NAME: return ReturnText("Example Sensor", value, valueElementCount, maxElementCount, encodingType);
			default: return false;
		}
	}
	if (propertyIdentifier == PROPERTY_OBJECT_NAME) {
		if (objectType == OBJECT_TYPE_ANALOG_INPUT && objectInstance == ANALOG_INPUT_INSTANCE) {
			return ReturnText("Room Temperature", value, valueElementCount, maxElementCount, encodingType);
		}
		if (objectType == OBJECT_TYPE_NETWORK_PORT && objectInstance == NETWORK_PORT_INSTANCE) {
			return ReturnText("BACnet/IP Port", value, valueElementCount, maxElementCount, encodingType);
		}
	}
	return false;
}

bool GetPropertyReal(const uint32_t deviceInstance, const uint16_t objectType,
	const uint32_t objectInstance, const uint32_t propertyIdentifier, float* value,
	const bool useArrayIndex, const uint32_t propertyArrayIndex, uint32_t* errorCode)
{
	(void)deviceInstance; (void)useArrayIndex; (void)propertyArrayIndex; (void)errorCode;
	if (objectType == OBJECT_TYPE_ANALOG_INPUT && objectInstance == ANALOG_INPUT_INSTANCE &&
		propertyIdentifier == PROPERTY_PRESENT_VALUE) {
		*value = g_roomTemperature;
		return true;
	}
	return false;
}

bool GetPropertyEnumerated(const uint32_t deviceInstance, const uint16_t objectType,
	const uint32_t objectInstance, const uint32_t propertyIdentifier, uint32_t* value,
	const bool useArrayIndex, const uint32_t propertyArrayIndex, uint32_t* errorCode)
{
	(void)deviceInstance; (void)useArrayIndex; (void)propertyArrayIndex; (void)errorCode;
	if (objectType == OBJECT_TYPE_ANALOG_INPUT && objectInstance == ANALOG_INPUT_INSTANCE &&
		propertyIdentifier == PROPERTY_UNITS) {
		*value = UNITS_DEGREES_CELSIUS;
		return true;
	}
	return false;
}

bool GetPropertyUnsignedInteger(const uint32_t deviceInstance, const uint16_t objectType,
	const uint32_t objectInstance, const uint32_t propertyIdentifier, uint32_t* value,
	const bool useArrayIndex, const uint32_t propertyArrayIndex, uint32_t* errorCode)
{
	(void)deviceInstance; (void)useArrayIndex; (void)propertyArrayIndex; (void)errorCode;
	if (objectType == OBJECT_TYPE_DEVICE && propertyIdentifier == PROPERTY_VENDOR_IDENTIFIER) {
		*value = 389; /* Replace with your own ASHRAE vendor identifier. */
		return true;
	}
	if (objectType == OBJECT_TYPE_NETWORK_PORT && objectInstance == NETWORK_PORT_INSTANCE &&
		propertyIdentifier == PROPERTY_BACNET_IP_UDP_PORT) {
		*value = g_udpPort;
		return true;
	}
	return false;
}

bool GetPropertyOctetString(const uint32_t deviceInstance, const uint16_t objectType,
	const uint32_t objectInstance, const uint32_t propertyIdentifier, uint8_t* value,
	uint32_t* valueElementCount, const uint32_t maxElementCount, const bool useArrayIndex,
	const uint32_t propertyArrayIndex, uint32_t* errorCode)
{
	(void)deviceInstance; (void)useArrayIndex; (void)propertyArrayIndex; (void)errorCode;
	if (objectType != OBJECT_TYPE_NETWORK_PORT || objectInstance != NETWORK_PORT_INSTANCE ||
		maxElementCount < 4) {
		return false;
	}
	const uint8_t* octets = NULL;
	switch (propertyIdentifier) {
		case PROPERTY_IP_ADDRESS: octets = g_ipAddress; break;
		case PROPERTY_IP_SUBNET_MASK: octets = g_ipSubnetMask; break;
		case PROPERTY_IP_DEFAULT_GATEWAY: octets = g_ipDefaultGateway; break;
		default: return false;
	}
	memcpy(value, octets, 4);
	*valueElementCount = 4;
	return true;
}

int main()
{
	if (!LoadBACnetFunctions()) {
		fprintf(stderr, "Cannot load the CAS BACnet Stack: %s\n", CASBACnetStackAdapter_LastError());
		return 1;
	}
	BACnetStack_RegisterCallbackLogDebugMessage(LogDebugMessage);

	if (!UdpOpen(g_udpPort, g_ipAddress, g_ipSubnetMask)) {
		return 1;
	}

	if (!BACnetStack_AddDevice(DEVICE_INSTANCE)) {
		return 1;
	}
	if (!BACnetStack_AddNetworkPortObject(DEVICE_INSTANCE, NETWORK_PORT_INSTANCE,
			5,         /* networkType: ipv4 */
			2,         /* protocolLevel: bacnetApplication */
			0,         /* networkNumber: not configured */
			0,         /* networkNumberQuality: unknown */
			4194303)) { /* referencePort: none, this is the lowest layer */
		return 1;
	}
	if (!BACnetStack_AddObject(DEVICE_INSTANCE, OBJECT_TYPE_ANALOG_INPUT, ANALOG_INPUT_INSTANCE)) {
		return 1;
	}

	BACnetStack_RegisterCallbackReceiveMessageForPort(ReceiveMessage);
	BACnetStack_RegisterCallbackSendMessageForPort(SendMessage);
	BACnetStack_RegisterCallbackGetSystemTime(GetSystemTime);
	BACnetStack_RegisterCallbackGetPropertyCharacterString(GetPropertyCharacterString);
	BACnetStack_RegisterCallbackGetPropertyReal(GetPropertyReal);
	BACnetStack_RegisterCallbackGetPropertyEnumerated(GetPropertyEnumerated);
	BACnetStack_RegisterCallbackGetPropertyUnsignedInteger(GetPropertyUnsignedInteger);
	BACnetStack_RegisterCallbackGetPropertyOctetString(GetPropertyOctetString);

	/* ReadProperty, Who-Is and Who-Has are on already. Add the services your device supports. */
	if (!BACnetStack_SetServiceEnabled(DEVICE_INSTANCE, 14 /* ReadPropertyMultiple */, true)) {
		return 1;
	}


	uint8_t broadcastConnectionString[6];
	for (int octetIndex = 0; octetIndex < 4; octetIndex++) {
		broadcastConnectionString[octetIndex] = (uint8_t)(g_ipAddress[octetIndex] | ~g_ipSubnetMask[octetIndex]);
	}
	broadcastConnectionString[4] = (uint8_t)(g_udpPort >> 8);
	broadcastConnectionString[5] = (uint8_t)(g_udpPort & 0xFF);

	BACnetStack_SendIAm(DEVICE_INSTANCE, broadcastConnectionString, 6, NETWORK_PORT_INSTANCE,
		true,   /* broadcast */
		0,      /* destinationNetwork: the local network */
		NULL, 0 /* destinationAddress: none */);

	for (;;) {
		BACnetStack_Tick();

		/* Your application's own work goes here: read sensors, update g_roomTemperature. */

#if defined(_WIN32)
		Sleep(10);
#else
		usleep(10 * 1000);
#endif
	}
	return 0;
}
