/*
 * sai-builder com-warmcat-sai client protocol implementation
 *
 * Copyright (C) 2019 Andy Green <andy@warmcat.com>
 *
 *  This library is free software; you can redistribute it and/or
 *  modify it under the terms of the GNU Lesser General Public
 *  License as published by the Free Software Foundation:
 *  version 2.1 of the License.
 *
 *  This library is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 *  Lesser General Public License for more details.
 *
 *  You should have received a copy of the GNU Lesser General Public
 *  License along with this library; if not, write to the Free Software
 *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
 *  MA  02110-1301  USA
 */

#include <libwebsockets.h>
#include <string.h>
#include <signal.h>

#include "private.h"

typedef enum {
	PHASE_IDLE,

	PFL_FIRST			= 	128,

	PHASE_START_ATTACH		=	PFL_FIRST | 1,
	PHASE_SUMM_TARGETS		=	2,
	PHASE_SUMM_NSPAWNS		=	3,
	PHASE_SUMM_NSPAWNS_OVERLAYS	=	4,

} cursor_phase_t;

struct pss {
	struct sai_target *target;
	struct sai_console *console;

	struct sai_nspawn *nspawn;
	struct sai_overlay *overlay;

	cursor_phase_t phase;
};

struct vhd {
	struct lws_context *context;
	struct lws_vhost *vhost;

	struct sai_builder builder;

	int *interrupted;
	const char **config_dir;
};

static int
generate(struct vhd *vhd, struct pss *pss, char *buf, int len)
{
	struct sai_builder *b = &vhd->builder;
	char *p = buf, *end = buf + len - 10;

	while (pss->phase != PHASE_IDLE && lws_ptr_diff(end, p) > 100) {

		switch (pss->phase) {
		case PHASE_IDLE:
			break;

		case PHASE_START_ATTACH:
			p += lws_snprintf(p, lws_ptr_diff(end, p),
					  "{\"schema\":\"com-warmcat-sai-ba\",\n"
					  " \"hostname\":\"%s\",\n"
					  " \"nspawn-timeout\":%d,\n"
					  " \"targets\": [",
					  lws_canonical_hostname(vhd->context),
					  b->nspawn_timeout);
			pss->target = lws_container_of(b->target_head.next,
						       struct sai_target,
						       target_list);
			pss->phase = PHASE_SUMM_TARGETS;
			break;

		case PHASE_SUMM_TARGETS:
			if (pss->target) {
				p += lws_snprintf(p, lws_ptr_diff(end, p),
						  "{\"name\":\"%s\"}",
						  pss->target->name);
				if (pss->target->target_list.next)
					*p++ = ',';
				*p++ = '\n';
				*p = '\0';

				pss->target = lws_container_of(
					pss->target->target_list.next,
					struct sai_target, target_list);
			}
			if (!pss->target) {
				p += lws_snprintf(p, lws_ptr_diff(end, p),
						  "],\n\"nspawns\": [");
				pss->nspawn = lws_container_of(
					b->nspawn_head.next,
					struct sai_nspawn, nspawn_list);
				pss->phase = PHASE_SUMM_NSPAWNS;
			}
			break;

		case PHASE_SUMM_NSPAWNS:
			if (pss->nspawn) {
				*p++ = '{';
				if (pss->nspawn->name)
					p += lws_snprintf(p, lws_ptr_diff(end, p),
							  "\"name\":\"%s\",\n",
							  pss->nspawn->name);
				p += lws_snprintf(p, lws_ptr_diff(end, p),
						  " \"overlays\": [");

				pss->overlay = lws_container_of(
					pss->nspawn->overlay_head.next,
					struct sai_overlay, overlay_list);
				if (pss->overlay) {
					pss->phase = PHASE_SUMM_NSPAWNS_OVERLAYS;
					break;
				}
next_nspawn:
				*p++ = ']';
				*p++ = '}';
				if (pss->nspawn->nspawn_list.next)
					*p++ = ',';
				*p++ = '\n';
				*p = '\0';
				pss->nspawn = lws_container_of(
					pss->nspawn->nspawn_list.next,
					struct sai_nspawn, nspawn_list);
			}
			if (!pss->nspawn) {
				p += lws_snprintf(p, lws_ptr_diff(end, p), "]\n");
				p += lws_snprintf(p, lws_ptr_diff(end, p), "}\n");
				pss->phase = PHASE_IDLE;
			}
			break;

		case PHASE_SUMM_NSPAWNS_OVERLAYS:
			if (pss->overlay) {
				p += lws_snprintf(p, lws_ptr_diff(end, p),
						  "{\"name\":\"%s\"}\n",
						  pss->overlay->name);
				pss->overlay = lws_container_of(
					pss->overlay->overlay_list.next,
					struct sai_overlay, overlay_list);
				if (pss->overlay) {

				}
			}
			if (!pss->overlay) {
				pss->phase = PHASE_SUMM_NSPAWNS;
				goto next_nspawn;
			}
			break;

		default:
			break;
		}
	}

	return lws_ptr_diff(p, buf);
}

static int
connect_client(struct vhd *vhd, struct sai_master *master)
{
	struct lws_client_connect_info i;
	const char *prot;
	char url[128], host[64];

	memset(&i, 0, sizeof(i));

	strncpy(url, master->url, sizeof(url) - 1);
	url[sizeof(url) - 1] = '\0';

	if (lws_parse_uri(url, &prot, &i.address, &i.port, &i.path)) {
		lwsl_err("%s: unable to parse url '%s'\n", __func__, url);

		return 1;
	}

	lws_snprintf(host, sizeof(host), "%s:%u", i.address, i.port);

	i.context = vhd->context;
	i.host = host;
	i.origin = host;

	if (!strcmp(prot, "https") || !strcmp(prot, "wss"))
		i.ssl_connection = LCCSCF_USE_SSL;
	if (master->accept_selfsigned)
		i.ssl_connection |= LCCSCF_ALLOW_SELFSIGNED;
	i.vhost = vhd->vhost;
	i.iface = master->interface;
	i.pwsi = &master->cwsi;
	i.opaque_user_data = (void *)master;

	lwsl_user("connecting to %s://%s:%d/%s\n", prot, i.address, i.port, i.path);

	return !lws_client_connect_via_info(&i);
}

static void
schedule_callback(struct lws *wsi, int reason, int secs)
{
	lws_timed_callback_vh_protocol(lws_get_vhost(wsi),
		lws_get_protocol(wsi), reason, secs);
}

static int
callback_com_warmcat_sai(struct lws *wsi, enum lws_callback_reasons reason,
			  void *user, void *in, size_t len)
{
	struct pss *pss = (struct pss *)user;
	struct vhd *vhd = (struct vhd *)
			lws_protocol_vh_priv_get(lws_get_vhost(wsi),
				lws_get_protocol(wsi));
	struct sai_master *master =
			(struct sai_master *)lws_get_opaque_user_data(wsi);
	uint8_t buf[1024 + LWS_PRE], *start = buf + LWS_PRE,
		*end = buf + sizeof(buf) -1;
	int n, m, f;

	switch (reason) {

	case LWS_CALLBACK_PROTOCOL_INIT:
		vhd = lws_protocol_vh_priv_zalloc(lws_get_vhost(wsi),
				lws_get_protocol(wsi),
				sizeof(struct vhd));
		if (!vhd)
			return -1;

		vhd->context = lws_get_context(wsi);
		vhd->vhost = lws_get_vhost(wsi);

		/* get the pointer to "interrupted" we were passed in pvo */
		vhd->interrupted = (int *)lws_pvo_search(
			(const struct lws_protocol_vhost_options *)in,
			"interrupted")->value;
		vhd->config_dir = (const char **)lws_pvo_search(
			(const struct lws_protocol_vhost_options *)in,
			"configdir")->value;

		if (sai_builder_config(&vhd->builder, *vhd->config_dir)) {
			lwsl_err("%s: config failed\n", __func__);

			return 1;
		}

		schedule_callback(wsi, LWS_CALLBACK_USER, 1);
		break;

	case LWS_CALLBACK_PROTOCOL_DESTROY:
		sai_builder_config_destroy(&vhd->builder);
		break;

	case LWS_CALLBACK_CLIENT_ESTABLISHED:
		pss->phase = PHASE_START_ATTACH;
		lws_callback_on_writable(wsi);
		break;

	case LWS_CALLBACK_CLIENT_WRITEABLE:
		lwsl_user("LWS_CALLBACK_CLIENT_WRITEABLE\n");

		if (pss->phase == PHASE_IDLE)
			break;

		f = pss->phase & PFL_FIRST;
		n = generate(vhd, pss, (char *)start, lws_ptr_diff(end, start));

		m = lws_write(wsi, start, n,
			      lws_write_ws_flags(LWS_WRITE_TEXT, f,
						 pss->phase == PHASE_IDLE));
		if (m < n) {
			lwsl_err("ERROR %d writing to ws socket\n", m);
			return -1;
		}

		if (pss->phase != PHASE_IDLE)
			lws_callback_on_writable(wsi);
		break;

	case LWS_CALLBACK_CLIENT_RECEIVE:

		break;

	case LWS_CALLBACK_CLIENT_CONNECTION_ERROR:
		lwsl_err("CLIENT_CONNECTION_ERROR: %s\n",
			 in ? (char *)in : "(null)");
		master->cwsi = NULL;
		schedule_callback(wsi, LWS_CALLBACK_USER, 1);
		break;

	case LWS_CALLBACK_CLIENT_CLOSED:
		lwsl_user("LWS_CALLBACK_CLIENT_CLOSED\n");
		master->cwsi = NULL;
		schedule_callback(wsi, LWS_CALLBACK_USER, 1);
		break;

	/* rate-limited client connect retries */

	case LWS_CALLBACK_USER:
		/*
		 * let's retry any master connections that aren't either
		 * ongoing or happy
		 */

		m = 0;
		lws_start_foreach_dll(struct lws_dll *, mp,
				      vhd->builder.master_head.next) {
			struct sai_master *ma = lws_container_of(mp,
						struct sai_master, master_list);

			if (!ma->cwsi && connect_client(vhd, ma))
				m = 1;
		} lws_end_foreach_dll(mp);

		/*
		 * if somebody's already unhappy, schedule a retry.  Otherwise
		 * wait until a connection close or error to do it then.
		 */
		if (m)
			schedule_callback(wsi, LWS_CALLBACK_USER, 1);
		break;

	default:
		break;
	}

	return 0;
}

const struct lws_protocols protocol_com_warmcat_sai = {
	"com-warmcat-sai",
	callback_com_warmcat_sai,
	sizeof(struct pss),
	1024, 0, NULL, 0
};