/*
 * Created by jiaqiang.ye@lnxall.com
 *
 * Simple OPC-UA remote TAG read/write
 *
 * 2024/08/23
 */


#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <math.h>

#include "open62541.h"
#include "opcua_set.h"
#include "remote_ua_client.h"

static void dump_tag(const char * tagname, const UA_Variant * var,
	const UA_LocalizedText * ltext, const UA_QualifiedName * qname)
{
	const char * textp, * namep;

	textp = (ltext->text.length > 0) ? (const char *) ltext->text.data : NULL;
	if (textp == NULL)
		textp = "";
	namep = (qname->name.length > 0) ? (const char *) qname->name.data : NULL;
	if (namep == NULL)
		namep = "";

	if (UA_Variant_isEmpty(var)) {
		fprintf(stdout, "%s => %s => %s: EMPTY\n", tagname, textp, namep);
		fflush(stdout);
		return;
	}

	if (UA_Variant_hasScalarType(var, &UA_TYPES[UA_TYPES_INT32])) {
		UA_Int32 ival = *((UA_Int32 *) var->data);
		fprintf(stdout, "%s => %s => %s: %d\n", tagname, textp, namep, ival);
		fflush(stdout);
		return;
	}

	if (UA_Variant_hasScalarType(var, &UA_TYPES[UA_TYPES_DOUBLE])) {
		UA_Double fval = *((UA_Double *) var->data);
		fprintf(stdout, "%s => %s => %s: %f\n", tagname, textp, namep, fval);
		fflush(stdout);
		return;
	}

	if (UA_Variant_hasScalarType(var, &UA_TYPES[UA_TYPES_STRING])) {
		const char * pstr = NULL;
		const UA_String * strp = (const UA_String *) var->data;
		if (strp != NULL && strp->length > 0)
			pstr = (const char *) strp->data;
		fprintf(stdout, "%s => %s => %s: %s\n", tagname, textp, namep, pstr ? : "[EMPTY]");
		fflush(stdout);
		return;
	}

	fprintf(stdout, "%s => %s => %s: UNKNOWN TYPE\n", tagname, textp, namep);
	fflush(stdout);
}

static void opcua_write_tag(UA_Client * uacli, const char * tagname,
	const UA_NodeId * pid, const char * valuep)
{
	int ret;
	char * endptr;
	UA_Variant var;

	endptr = NULL;
	UA_Variant_init(&var);
	/* read old value first */
	ret = UA_Client_readValueAttribute(uacli, *pid, &var);
	if (ret != UA_STATUSCODE_GOOD) {
		fprintf(stderr, "Error, failed to query OPCUA: %s\n", tagname);
		fflush(stderr);
		return;
	}

	ret = UA_STATUSCODE_GOOD;
	if (UA_Variant_hasScalarType(&var, &UA_TYPES[UA_TYPES_INT32])) {
		int ival;
		ival = (int) strtol(valuep, &endptr, 0);
		if (valuep == endptr) {
			fprintf(stderr, "Error, invalid value for '%s', INT32 expected.\n", tagname);
			fflush(stderr);
		} else {
			UA_Variant_clear(&var);
			UA_Variant_setScalarCopy(&var, &ival, &UA_TYPES[UA_TYPES_INT32]);
			ret = UA_Client_writeValueAttribute(uacli, *pid, &var);
			if (ret == UA_STATUSCODE_GOOD) {
				fprintf(stdout, "Updated '%s' => %d\n", tagname, ival);
				fflush(stderr);
			}
		}
	} else if (UA_Variant_hasScalarType(&var, &UA_TYPES[UA_TYPES_DOUBLE])) {
		double dval;
		dval = strtod(valuep, &endptr);
		if (valuep == endptr || isnan(dval) || isinf(dval)) {
			fprintf(stderr, "Error, invalid value for '%s', DOUBLE expected.\n", tagname);
			fflush(stderr);
		} else {
			UA_Variant_clear(&var);
			UA_Variant_setScalarCopy(&var, &dval, &UA_TYPES[UA_TYPES_DOUBLE]);
			ret = UA_Client_writeValueAttribute(uacli, *pid, &var);
			if (ret == UA_STATUSCODE_GOOD) {
				fprintf(stdout, "Updated '%s' => %f\n", tagname, dval);
				fflush(stderr);
			}
		}
	} else if (UA_Variant_hasScalarType(&var, &UA_TYPES[UA_TYPES_STRING])) {
		UA_String newval;
		UA_Variant_clear(&var);
		newval = UA_STRING((char *) valuep);
		UA_Variant_setScalarCopy(&var, &newval, &UA_TYPES[UA_TYPES_STRING]);
		ret = UA_Client_writeValueAttribute(uacli, *pid, &var);
		if (ret == UA_STATUSCODE_GOOD) {
			fprintf(stdout, "Updated '%s' => %s\n", tagname, valuep);
			fflush(stderr);
		}
	} else {
		fprintf(stderr, "Error, invalid OPC-UA tag '%s': UNKNOWN TYPE\n", tagname);
		fflush(stderr);
	}

	if (ret != UA_STATUSCODE_GOOD) {
		fprintf(stderr, "Error, failed to update TAG '%s': %s\n",
			tagname, UA_StatusCode_name(ret));
		fflush(stderr);
	}
	UA_Variant_clear(&var);
}

static void opcua_cli_usage(const char *arg0)
{
	fprintf(stderr, "Usage: %s [-i 127.0.0.1] [-p 4840] [-u admin] [-P lnxall123]"
		" [-n 1] -s|-g KEY1 [VALUE1] KEY2 [VALUE2]\n", arg0);
	fputs("\t-i IPADDRESS -- OPC-UA server IP address, default: 127.0.0.1\n", stderr);
	fputs("\t-p PORTNO    -- OPC-UA server port number, default: 4840\n", stderr);
	fputs("\t-u USERNAME  -- OPC-UA login user name, default: admin\n", stderr);
	fputs("\t-P PASSWORD  -- OPC-UA login password, default: lnxall123\n", stderr);
	fputs("\t-n NAMESPACE -- OPC-UA namespace index, default: 1\n", stderr);
	fputs("\t-s           -- Set OPC-UA tag value\n", stderr);
	fputs("\t-g           -- Get OPC-uA tag value\n", stderr);
	fflush(stderr);
	exit(1);
}

int main(int argc, char *argv[])
{
	UA_UInt16 uans;
	UA_Client * uacli;
	struct opcua_setopt uaopt;
	struct cli_opcua_option cliop;
	int ret, args, eval, query, doset;

	uans = 1;
	uacli = NULL;
	eval = query = doset = 0;
	setopt_init(&uaopt);
	lc_opcua_option_default(&cliop);

	for (;;) {
		int tmpval;
		ret = getopt(argc, argv, "i:p:u:P:n:gsh");
		if (ret < 0)
			break;

		switch (ret) {
		case 'h':
			opcua_cli_usage(argv[0]);
			break;

		case 's':
			doset++;
			break;

		case 'g':
			query++;
			break;

		case 'i':
			setopt_update_ipport(&uaopt, optarg, 0);
			break;

		case 'p':
			tmpval = (optarg != NULL) ? (int) strtol(optarg, NULL, 0) : -1;
			if (tmpval > 0)
				setopt_update_ipport(&uaopt, NULL, tmpval);
			break;

		case 'u':
			setopt_update_userpass(&uaopt, optarg, NULL);
			break;

		case 'P':
			setopt_update_userpass(&uaopt, NULL, optarg);
			break;

		case 'n':
			tmpval = (optarg != NULL) ? (int) strtol(optarg, NULL, 0) : -1;
			if (tmpval > 0)
				uans = (UA_UInt16) tmpval;
			break;

		default:
			fprintf(stderr, "Error, unsupported option: %c (%#x)\n",
				ret, (unsigned int) ret);
			fflush(stderr);
			break;
		}
	}

	if ((doset == 0 && query == 0) || (doset != 0 && query != 0))
		opcua_cli_usage(argv[0]);

	args = optind;
	if (args <= 0 || args >= argc) {
		eval = 1;
		fputs("Error, no arguments found.\n", stderr);
		fflush(stderr);
		goto err3;
	}

	cliop.uaurl      = uaopt.uaurl;
	cliop.username   = uaopt.username;
	cliop.passwd     = uaopt.passwd;
	uacli = lc_connect_opcua(&cliop);
	if (uacli == NULL) {
		eval = 2;
		goto err3;
	}

	if (query != 0) {
		for (; args < argc; ++args) {
			UA_NodeId nid;
			const char * tag;
			UA_Variant var;
			UA_LocalizedText ltext;
			UA_QualifiedName qname;

			tag = argv[args];
			if (tag == NULL || tag[0] == '\0')
				continue;

			nid = UA_NODEID_STRING(uans, (char *) tag);
			UA_LocalizedText_init(&ltext);
			ret = UA_Client_readDisplayNameAttribute(uacli, nid, &ltext);
			if (ret != UA_STATUSCODE_GOOD) {
				fprintf(stderr, "Error, failed to read displayName for '%s'\n", tag);
				fflush(stderr);
				goto err2;
			}

			UA_QualifiedName_init(&qname);
			ret = UA_Client_readBrowseNameAttribute(uacli, nid, &qname);
			if (ret != UA_STATUSCODE_GOOD) {
				fprintf(stderr, "Error, failed to read browsename for '%s'\n", tag);
				fflush(stderr);
				goto err1;
			}

			UA_Variant_init(&var);
			ret = UA_Client_readValueAttribute(uacli, nid, &var);
			if (ret != UA_STATUSCODE_GOOD) {
				fprintf(stderr, "Error, failed to query OPCUA: %s\n", tag);
				fflush(stderr);
				goto err0;
			}

			dump_tag(tag, &var, &ltext, &qname);

err0:
			UA_Variant_clear(&var);
err1:
			UA_QualifiedName_clear(&qname);
err2:
			UA_LocalizedText_clear(&ltext);
		}
		goto err3;
	}

	for (; args < argc; args += 2) {
		UA_NodeId nid;
		const char * tag, * tagv;
		tag = argv[args];
		if (tag == NULL)
			break;
		tagv = argv[args + 1];
		if (tagv == NULL)
			break;

		nid = UA_NODEID_STRING(uans, (char *) tag);
		opcua_write_tag(uacli, tag, &nid, tagv);
	}

err3:
	setopt_free(&uaopt);
	if (uacli != NULL)
		lc_client_free(uacli);
	return eval;
}

int setopt_init(struct opcua_setopt * opt)
{
	opt->username   = strdup("admin");
	opt->passwd     = strdup("lnxall123");

	opt->uaurl      = NULL;
	opt->serverip   = NULL;
	opt->portno     = 4840;
	return setopt_update_ipport(opt, "127.0.0.1", 0);
}

int setopt_update_ipport(struct opcua_setopt * opt,
	const char * serverip, int portno)
{
	int reset = 0;
	if (serverip && serverip[0]) {
		if (opt->serverip != NULL)
			free(opt->serverip);
		reset++;
		opt->serverip = strdup(serverip);
	}

	if (portno > 0 && portno < 65536) {
		reset++;
		opt->portno = portno;
	}

	if (reset != 0 || opt->uaurl == NULL) {
		int ret;
		if (opt->uaurl != NULL) {
			free(opt->uaurl);
			opt->uaurl = NULL;
		}
		ret = asprintf(&opt->uaurl, "opc.tcp://%s:%d",
			opt->serverip ? : "127.0.0.1", opt->portno);
		if (ret < 0) {
			opt->uaurl = NULL;
			fputs("Error, failed to format UAURL!\n", stderr);
			fflush(stderr);
			return -1;
		}
	}
	return 0;
}

int setopt_update_userpass(struct opcua_setopt * opt,
	const char * username, char * passwd)
{
	if (username && username[0]) {
		if (opt->username != NULL)
			free(opt->username);
		opt->username = strdup(username);
	}

	if (passwd && passwd[0]) {
		if (opt->passwd != NULL)
			free(opt->passwd);
		opt->passwd = strdup(passwd);
	}
	return 0;
}

void setopt_free(struct opcua_setopt * opt)
{
	if (opt == NULL)
		return;

	if (opt->uaurl != NULL) {
		free(opt->uaurl);
		opt->uaurl = NULL;
	}

	if (opt->username != NULL) {
		free(opt->username);
		opt->username = NULL;
	}

	if (opt->passwd != NULL) {
		free(opt->passwd);
		opt->passwd = NULL;
	}

	if (opt->serverip != NULL) {
		free(opt->serverip);
		opt->serverip = NULL;
	}

	opt->portno = -1;
}
