/*
 * lws-minimal-dbus-ws-proxy-testclient
 *
 * Written in 2010-2019 by Andy Green <andy@warmcat.com>
 *
 * This file is made available under the Creative Commons CC0 1.0
 * Universal Public Domain Dedication.
 *
 * This acts as a test client over DBUS, opening a session with
 * minimal-dbus-ws-proxy and sending and receiving data on the libwebsockets
 * mirror demo page.
 */

#include <stdbool.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>

#include <libwebsockets.h>

enum {
	LWS_SW_D,
	LWS_SW_X,
	LWS_SW_HELP,
};

static const struct lws_switches switches[] = {
	[LWS_SW_D]	= { "-d",              "Debug logs (e.g. -d 15)" },
	[LWS_SW_X]	= { "-x",              "Enable -x feature" },
	[LWS_SW_HELP]	= { "--help",		"Show this help information" },
};

#include <libwebsockets/lws-dbus.h>

/*
 * These are the various states our connection can be in, both with regards
 * to the direct connection to the proxy, and the state of the onward ws
 * connection the proxy opens at our request.
 */

enum lws_dbus_client_state {
	LDCS_NOTHING,		  /* no connection yet */
	LDCS_CONN,		  /* conn to proxy */
	LDCS_CONN_WAITING_ONWARD, /* conn to proxy, awaiting proxied conn */
	LDCS_CONN_ONWARD,	  /* conn to proxy and onward conn OK */
	LDCS_CONN_CLOSED,	  /* conn to proxy but onward conn closed */
	LDCS_CLOSED,		  /* connection to proxy is closed */
};

/*
 * our expanded dbus context
 */

struct lws_dbus_ctx_wsproxy_client {
	struct lws_dbus_ctx ctx;

	lws_sorted_usec_list_t sul;

	enum lws_dbus_client_state state;
};

static struct lws_dbus_ctx_wsproxy_client *dbus_ctx;
static struct lws_context *context;
static int interrupted, autoexit_budget = -1, count_rx, count_tx;

#define THIS_INTERFACE	 "org.libwebsockets.wsclientproxy"
#define THIS_OBJECT	 "/org/libwebsockets/wsclientproxy"
#define THIS_BUSNAME	 "org.libwebsockets.wsclientproxy"

#define THIS_LISTEN_PATH "unix:abstract=org.libwebsockets.wsclientproxy"

static void
state_transition(struct lws_dbus_ctx_wsproxy_client *dcwc,
		 enum lws_dbus_client_state state)
{
	lwsl_notice("%s: %p: from state %d -> %d\n", __func__,
		    dcwc,dcwc->state, state);
	dcwc->state = state;
}

static DBusHandlerResult
filter(DBusConnection *conn, DBusMessage *message, void *data)
{
	struct lws_dbus_ctx_wsproxy_client *dcwc =
			(struct lws_dbus_ctx_wsproxy_client *)data;
	const char *str;

	if (!dbus_message_get_args(message, NULL,
				   DBUS_TYPE_STRING, &str,
				   DBUS_TYPE_INVALID))
		return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;

	/* received ws data */

	if (dbus_message_is_signal(message, THIS_INTERFACE, "Receive")) {
		lwsl_user("%s: Received '%s'\n", __func__, str);
		count_rx++;
	}

	/* proxy ws connection failed */

	if (dbus_message_is_signal(message, THIS_INTERFACE, "Status") &&
	    !strcmp(str, "ws client connection error"))
		state_transition(dcwc, LDCS_CONN_CLOSED);

	/* proxy ws connection succeeded */

	if (dbus_message_is_signal(message, THIS_INTERFACE, "Status") &&
	    !strcmp(str, "ws client connection established"))
		state_transition(dcwc, LDCS_CONN_ONWARD);

	/* proxy ws connection has closed */

	if (dbus_message_is_signal(message, THIS_INTERFACE, "Status") &&
	    !strcmp(str, "ws client connection closed"))
		state_transition(dcwc, LDCS_CONN_CLOSED);

	return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
}

static void
destroy_dbus_client_conn(struct lws_dbus_ctx_wsproxy_client **pdcwc)
{
	struct lws_dbus_ctx_wsproxy_client *dcwc = *pdcwc;

	if (!dcwc || !dcwc->ctx.conn)
		return;

	lwsl_notice("%s\n", __func__);

	dbus_connection_remove_filter(dcwc->ctx.conn, filter, &dcwc->ctx);
	dbus_connection_close(dcwc->ctx.conn);
	dbus_connection_unref(dcwc->ctx.conn);

	free(dcwc);

	*pdcwc = NULL;
}

/*
 * This callback is coming when lws has noticed the fd took a POLLHUP.  The
 * ctx has effectively gone out of scope before this, and the connection can
 * be cleaned up and the ctx freed.
 */

static void
cb_closing(struct lws_dbus_ctx *ctx)
{
	struct lws_dbus_ctx_wsproxy_client *dcwc =
			(struct lws_dbus_ctx_wsproxy_client *)ctx;

	lwsl_err("%s: closing\n", __func__);

	if (dcwc == dbus_ctx)
		dbus_ctx = NULL;

	destroy_dbus_client_conn(&dcwc);

	interrupted = 1;
}

static struct lws_dbus_ctx_wsproxy_client *
create_dbus_client_conn(struct lws_vhost *vh, int tsi, const char *ads)
{
	struct lws_dbus_ctx_wsproxy_client *dcwc;
	DBusError e;

	dcwc = malloc(sizeof(*dcwc));
	if (!dcwc)
		return NULL;

	memset(dcwc, 0, sizeof(*dcwc));

	dcwc->state = LDCS_NOTHING;
	dcwc->ctx.vh = vh;
	dcwc->ctx.tsi = tsi;

        dbus_error_init(&e);

        lwsl_user("%s: connecting to '%s'\n", __func__, ads);
#if 1
	/* connect to our daemon bus */

        dcwc->ctx.conn = dbus_connection_open_private(ads, &e);
	if (!dcwc->ctx.conn) {
		lwsl_err("%s: Failed to connect: %s\n",
			 __func__, e.message);
		goto fail;
	}
#else
	/* connect to the SYSTEM bus */

	dcwc->ctx.conn = dbus_bus_get(DBUS_BUS_SYSTEM, &e);
	if (!dcwc->ctx.conn) {
		lwsl_err("%s: Failed to get a session DBus connection: %s\n",
			 __func__, e.message);
		goto fail;
	}
#endif
	dbus_connection_set_exit_on_disconnect(dcwc->ctx.conn, 0);

	if (!dbus_connection_add_filter(dcwc->ctx.conn, filter,
					&dcwc->ctx, NULL)) {
		lwsl_err("%s: Failed to add filter\n", __func__);
		goto fail;
	}

	/*
	 * This is the part that binds the connection to lws watcher and
	 * timeout handling provided by lws
	 */

	if (lws_dbus_connection_setup(&dcwc->ctx, dcwc->ctx.conn, cb_closing)) {
		lwsl_err("%s: connection bind to lws failed\n", __func__);
		goto fail;
	}

	state_transition(dcwc, LDCS_CONN);

	lwsl_notice("%s: created OK\n", __func__);

	return dcwc;

fail:
	dbus_error_free(&e);

	free(dcwc);

	return NULL;
}


void sigint_handler(int sig)
{
	interrupted = 1;
}

/*
 * This gets called if we timed out waiting for the dbus server reply, or the
 * reply arrived.
 */

static void
pending_call_notify(DBusPendingCall *pending, void *data)
{
	const char *payload;
	DBusMessage *msg;

	if (!dbus_pending_call_get_completed(pending)) {
		lwsl_err("%s: timed out waiting for reply\n", __func__);

		goto bail;
	}

	msg = dbus_pending_call_steal_reply(pending);
	if (!msg)
		goto bail;

	if (!dbus_message_get_args(msg, NULL, DBUS_TYPE_STRING, &payload,
					      DBUS_TYPE_INVALID)) {
		goto bail1;
	}

	lwsl_user("%s: received '%s'\n", __func__, payload);

bail1:
	dbus_message_unref(msg);
bail:
	dbus_pending_call_unref(pending);
}

static int
remote_method_call(struct lws_dbus_ctx_wsproxy_client *dcwc)
{
	char _uri[96];
	const char *subprotocol = "lws-mirror-protocol", *uri = _uri;
	DBusMessage *msg;
	int ret = 1;

	/*
	 * make our own private mirror session... because others may run this
	 * at the same time against libwebsockets.org... as happened 2019-03-14
	 * and broke travis tests :-)
	 */

	lws_snprintf(_uri, sizeof(_uri), "wss://libwebsockets.org/?mirror=dbt-%d",
			(int)getpid());

	msg = dbus_message_new_method_call(
			/* dest */	  THIS_BUSNAME,
			/* object-path */ THIS_OBJECT,
			/* interface */   THIS_INTERFACE,
			/* method */	  "Connect");
	if (!msg)
		return 1;

	if (!dbus_message_append_args(msg, DBUS_TYPE_STRING, &uri,
					   DBUS_TYPE_STRING, &subprotocol,
					   DBUS_TYPE_INVALID))
		goto bail;

	lwsl_user("%s: requesting proxy connection %s %s\n", __func__,
			uri, subprotocol);

	if (!dbus_connection_send_with_reply(dcwc->ctx.conn, msg, &dcwc->ctx.pc,
					     DBUS_TIMEOUT_USE_DEFAULT)) {
		lwsl_err("%s: unable to send\n", __func__);

		goto bail;
	}

	dbus_pending_call_set_notify(dcwc->ctx.pc, pending_call_notify,
				     &dcwc->ctx, NULL);

	state_transition(dcwc, LDCS_CONN_WAITING_ONWARD);

	ret = 0;

bail:
	dbus_message_unref(msg);

	return ret;
}

static void
sul_timer(struct lws_sorted_usec_list *sul)
{
	char payload[64];
	const char *ws_pkt = payload;
	DBusMessage *msg;
	uint32_t r[5];

	if (!dbus_ctx || dbus_ctx->state != LDCS_CONN_ONWARD)
		goto again;

	if (autoexit_budget > 0) {
		if (!--autoexit_budget) {
			lwsl_notice("reached autoexit budget\n");
			interrupted = 1;
			return;
		}
	}

	msg = dbus_message_new_method_call(THIS_BUSNAME, THIS_OBJECT,
					   THIS_INTERFACE, "Send");
	if (!msg)
		goto again;

	lws_get_random(context, r, sizeof(r));

	lws_snprintf(payload, sizeof(payload), "d #%06X %d %d %d %d;",
		     r[0] & 0xffffff, r[1] % 480, r[2] % 300,
		     r[3] % 480, r[4] % 300);

	if (!dbus_message_append_args(msg, DBUS_TYPE_STRING, &ws_pkt,
					   DBUS_TYPE_INVALID)) {
		dbus_message_unref(msg);
		goto again;
	}

	if (!dbus_connection_send_with_reply(dbus_ctx->ctx.conn, msg,
					     &dbus_ctx->ctx.pc,
					    DBUS_TIMEOUT_USE_DEFAULT)) {
		lwsl_err("%s: unable to send\n", __func__);
		dbus_message_unref(msg);
		goto again;
	}

	dbus_message_unref(msg);
	dbus_pending_call_set_notify(dbus_ctx->ctx.pc,
				     pending_call_notify,
				     &dbus_ctx->ctx, NULL);
	count_tx++;

again:
	lws_sul_schedule(context, 0, &dbus_ctx->sul, sul_timer, 2 * LWS_US_PER_SEC);
}

int main(int argc, const char **argv)
{
	struct lws_vhost *vh;
	struct lws_context_creation_info info;
	const char *p;
	int n = 0;
	(void)switches;

	if ((argc == 1) || lws_cmdline_option(argc, argv, switches[LWS_SW_HELP].sw)) {
		lws_switches_print_help(argv[0], switches, LWS_ARRAY_SIZE(switches));
		return 0;
	}


	signal(SIGINT, sigint_handler);


	if ((p = lws_cmdline_option(argc, argv, switches[LWS_SW_X].sw)))
		autoexit_budget = atoi(p);

	lwsl_user("LWS minimal DBUS ws proxy testclient\n");

	lws_context_info_defaults(&info, NULL);
	lws_cmdline_option_handle_builtin(argc, argv, &info);
	info.options = LWS_SERVER_OPTION_EXPLICIT_VHOSTS;
	context = lws_create_context(&info);
	if (!context) {
		lwsl_err("lws init failed\n");
		return 1;
	}

	info.options |=
		LWS_SERVER_OPTION_HTTP_HEADERS_SECURITY_BEST_PRACTICES_ENFORCE;

	vh = lws_create_vhost(context, &info);
	if (!vh)
		goto bail;

	dbus_ctx = create_dbus_client_conn(vh, 0, THIS_LISTEN_PATH);
	if (!dbus_ctx)
		goto bail1;

	lws_sul_schedule(context, 0, &dbus_ctx->sul, sul_timer, LWS_US_PER_SEC);


	if (remote_method_call(dbus_ctx))
		goto bail2;

	/* lws event loop (default poll one) */

	while (n >= 0 && !interrupted)
		n = lws_service(context, 0);

bail2:
	destroy_dbus_client_conn(&dbus_ctx);

bail1:
	/* this is required for valgrind-cleanliness */
	dbus_shutdown();
	lws_context_destroy(context);

	lwsl_notice("Exiting cleanly, rx: %d, tx: %d\n", count_rx, count_tx);

	return 0;

bail:
	lwsl_err("%s: failed to start\n", __func__);
	lws_context_destroy(context);

	return 1;
}
