/*
 *                            COPYRIGHT
 *
 *  pcb-rnd, interactive printed circuit board design
 *  Copyright (C) 2025 Tibor 'Igor2' Palinkas
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation; either version 2 of the License, or
 *  (at your option) any later version.
 *
 *  This program 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 General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program; if not, write to the Free Software
 *  Foundation, Inc., 31 Milk Street, # 960789 Boston, MA 02196 USA.
 *
 *  Contact:
 *    Project page: http://www.repo.hu/projects/librnd
     lead developer: http://www.repo.hu/projects/librnd/contact.html
 *    mailing list: pcb-rnd (at) list.repo.hu (send "subscribe")
 */

#include <librnd/rnd_config.h>
#include <stdlib.h>
#include <ctype.h>
#include "config.h"
#include <time.h>
#include <genlist/gendlist.h>
#include <genvector/gds_char.h>
#include <librnd/hid/hid.h>
#include <librnd/hid/hid_attrib.h>
#include <librnd/core/actions.h>
#include <librnd/core/event.h>
#include <librnd/core/plugins.h>
#include <librnd/core/compat_misc.h>
#include <librnd/core/hidlib.h>
#include <libporty_net/libportytcp4.h>

#define PERIOD_MS          333
#define CONN_TIMEOUT_S     3
#define RECONN_TIMEOUT_S   3
#define GROW 512

/* Environmental variable that isntructs the application to connect to
   LOCALHOST:port for tcp remote controlling by a project manager */
#define ENV_VAR    "PRJ_RND_PORT"

/* The project manager is typically running on the same machine; this
   hostname (or IP address) is relatively safe to connect to */
#define LOCALHOST  "127.0.0.1"

typedef enum remconn_st_e {
	RCST_DISCONNECTED = 1,  /* not yet connecting */
	RCST_CONNECTING,        /* trying to connect */
	RCST_NORMAL             /* already connected, waiting for actions */
} remconn_st_t;

typedef struct remconn_s {
	P_net_socket sk;
	remconn_st_t st;

	time_t next_reconn; /* when to reconnect next */

	char *host; /* NULL or empty means LOCALHOST (safe) */
	int port;

	gds_t ibuf, obuf;  /* unflushed input/output buffer content; ibuf is partial line, obuf is waiting for send() */

	gdl_elem_t link;   /* in list of all remote connections */
} remconn_t;

/* 1 if the timer is running (there's at least one remote connection to watch for) */
static int timer_running;

/* list of all remote connections */
static gdl_list_t remconns;

static void remctrl_tcp_write(remconn_t *conn);

/* returns if host argument matches conn->host */
static int conn_host_match(const remconn_t *conn, const char *host)
{
	if (conn->host == NULL) {
		if (host == NULL) return 1;
		if (rnd_strcasecmp(host, LOCALHOST) == 0) return 1;
		return 0;
	}
	if (host == NULL) {
		if (conn->host == NULL) return 1;
		if (rnd_strcasecmp(conn->host, LOCALHOST) == 0) return 1;
		return 0;
	}

	return (rnd_strcasecmp(conn->host, host) == 0);
}

/* Free all memory used by conn */
static void remctrl_tcp_free_conn(remconn_t *conn)
{
	free(conn->host);
	gds_uninit(&conn->ibuf);
	gds_uninit(&conn->obuf);
	free(conn);
}

/* Discard a remoconn and never try to connect it again */
static void remctrl_tcp_remove(remconn_t *conn)
{
	if (conn->st != RCST_DISCONNECTED)
		P_net_close(conn->sk);

	gdl_remove(&remconns, conn, link);
	remctrl_tcp_free_conn(conn);

	/* stop the timer if there are no more connections to watch */
	if (remconns.first == NULL)
		timer_running = 0; /* stops the timer at the next run */
}

/* attempt to (re)connect to remote server */
static void remctrl_tcp_reconn(remconn_t *conn, time_t now)
{
	const char *host = conn->host;

	if ((host == NULL) || (*host == '\0'))
		host = LOCALHOST;

	rnd_trace("remctrl_tcp: connecting %s:%d...\n", host, conn->port);
	P_tcp4_connect(&conn->sk, host, conn->port, NULL, 0, P_net_nonblock_full);

	conn->st = RCST_CONNECTING;
	conn->next_reconn = now + CONN_TIMEOUT_S;
}

/* Writes the hello message (package name and version) so that the remote
   controller knows what to do with the connection */
static void write_hello(remconn_t *conn)
{
	gds_append_len(&conn->obuf, "hello ", 6);
	gds_append_str(&conn->obuf, rnd_app.package);
	gds_append(&conn->obuf, '\t');
	gds_append_str(&conn->obuf, rnd_app.version);
	gds_append(&conn->obuf, '\n');
	remctrl_tcp_write(conn); /* flush output */
}

/* Handle the poll for a remconn that is connecting */
static void remctrl_tcp_connecting(remconn_t *conn, time_t now)
{
	struct P_pollfd fds[1];

	fds[0].fd = conn->sk;
	fds[0].events = P_POLLOUT;

	/* check if already connected (writable) */
	if (P_poll(fds, 1, 0) == 1) {
		if (fds[0].revents & P_POLLOUT) {
			P_size_t len = P_net_write(conn->sk, "\n", 1); /* empty lines are ignored on the server side */
			if (len == 1) {
				rnd_trace("remctrl_tcp: connected!\n");
				conn->ibuf.used = conn->obuf.used = 0;
				conn->st = RCST_NORMAL;
				write_hello(conn);
				return;
			}
			else {
				rnd_trace("remctrl_tcp: failed to connect\n");
				goto reconn;
			}
		}
	}

	/* verify timeout if not and restart */
	if (now >= conn->next_reconn) {
		rnd_trace("remctrl_tcp: connection timeout\n");

		/* schedule reconnect for later */
		reconn:;
		conn->next_reconn = now + RECONN_TIMEOUT_S;
		P_net_close(conn->sk);
		conn->st = RCST_DISCONNECTED;
	}
}

/* Handle the poll for a remconn that is connected and readable */
static void remctrl_tcp_read(remconn_t *conn)
{
	long len, oused = conn->ibuf.used;
	char *end;
	rnd_design_t *dsg = NULL;


	gds_enlarge(&conn->ibuf, conn->ibuf.used + GROW);
	conn->ibuf.used = oused;
	len = P_net_read(conn->sk, conn->ibuf.array + conn->ibuf.used, GROW);


	if (len < 0) {
		if (P_net_inprogress)
				return;
		rnd_trace("remctrl_tcp: error reading the socket\n");
		reconn:;
		conn->next_reconn = time(NULL) + RECONN_TIMEOUT_S;
		P_net_close(conn->sk);
		conn->st = RCST_DISCONNECTED;
		return;
	}

	if (len == 0) {
		rnd_trace("remctrl_tcp: EOF reading the socket\n");
		goto reconn;
	}

	if (len > 0)
		rnd_trace("remctrl_tcp: read %ld\n", len);

	if (conn->ibuf.array == NULL)
		return;

	conn->ibuf.used = len;

	/* parse commands, manage the buffer */
	for(;;) {
		long rem;

		end = strpbrk(conn->ibuf.array, "\r\n");
		if (end == NULL)
			return;
		*end = '\0';
		rnd_trace("  cmd='%s'\n", conn->ibuf.array);

		if (strcmp(conn->ibuf.array, "bye") == 0) {
			remctrl_tcp_remove(conn);
			return;
		}

		if (dsg == NULL)
			dsg = rnd_multi_get_current();

		if (dsg != NULL)
			rnd_parse_actions(dsg, conn->ibuf.array);

		end++;
		while(isspace(*end)) end++;
		rem = (long)conn->ibuf.used - (end - conn->ibuf.array);
		if (rem < 0)
			rem = 0;
		if (rem > 0)
			memmove(conn->ibuf.array, end, rem);
		conn->ibuf.array[rem] = '\0';
		conn->ibuf.used = rem;
	}
}

/* Handle the poll for a remconn that is connected and writable */
static void remctrl_tcp_write(remconn_t *conn)
{
	long wlen;

	if (conn->obuf.used <= 0)
		return; /* no pending writes */

	/* attempt to write the whole obuf */
	wlen = P_net_write(conn->sk, conn->obuf.array, conn->obuf.used);
	if (wlen > 0) {
		if (wlen >= conn->obuf.used) { /* all written, empty obuf */
			conn->obuf.array[0] = '\0';
			conn->obuf.used = 0;
		}
		else { /* partial write, shift remaining content in obuf */
			long remain = conn->obuf.used - wlen;
			memmove(conn->obuf.array, conn->obuf.array+wlen, remain);
			conn->obuf.used = remain;
		}
	}

	/* No need to handle error: remctrl_tcp_read() will figure if the connection
	   is broken */
}


/* Poll at each active remconn, doing whatever needs to be done in the given state */
static void remctrl_tcp_poll(void)
{
	time_t now = 0;
	remconn_t *conn, *nextc;


	for(conn = gdl_first(&remconns); conn != NULL; conn = nextc) {

		/* remember next conn because current conn may be removed */
		nextc = gdl_next(&remconns, conn);

		switch(conn->st) {
			case RCST_DISCONNECTED:
				if (now == 0)
					now = time(NULL);
				if (now >= conn->next_reconn)
					remctrl_tcp_reconn(conn, now);
				break;
			case RCST_CONNECTING:
				if (now == 0)
					now = time(NULL);
				remctrl_tcp_connecting(conn, now);
				break;
			case RCST_NORMAL:
				remctrl_tcp_write(conn);
				remctrl_tcp_read(conn); /* may remove connection */
				break;
		}
	}
}

static void timer_cb(rnd_hidval_t hv)
{
	if (timer_running) {
		remctrl_tcp_poll();
		rnd_gui->add_timer(rnd_gui, timer_cb, PERIOD_MS, hv);
	}
}

static void timer_maybe_start(void)
{
	rnd_hidval_t hv = {0};
	if (timer_running)
		return;


	timer_running = 1;
	timer_cb(hv);
}

/* Register a new remconn for host:port */
static void remctrl_tcp_connect(const char *host, int port)
{
	remconn_t *conn = calloc(sizeof(remconn_t), 1);

	/* set up the connection for reconnect on next timer */
	conn->st = RCST_DISCONNECTED;
	conn->host = host == NULL ? NULL : rnd_strdup(host);
	conn->port = port;
	conn->next_reconn = 0;

	gdl_append(&remconns, conn, link);

	/* make sure the timer is running (relevant for the first connection) */
	timer_maybe_start();
}

const char rnd_acts_remctrl_tcp_req[] = "remctrl_tcp_req(str)";
const char rnd_acth_remctrl_tcp_req[] = "print a request string to all currently connected remctrl tcp sockets; may contain a single line or multiple lines of raw messages";
fgw_error_t rnd_act_remctrl_tcp_req(fgw_arg_t *res, int argc, fgw_arg_t *argv)
{
	remconn_t *conn;
	char *cmds;
	int cnt = 0;

	RND_ACT_CONVARG(1, FGW_STR, remctrl_tcp_req, cmds = argv[1].val.str);

	for(conn = gdl_first(&remconns); conn != NULL; conn = gdl_next(&remconns, conn)) {
		if (conn->st == RCST_NORMAL) {
			gds_append_str(&conn->obuf, cmds);
			gds_append(&conn->obuf, '\n');
			remctrl_tcp_write(conn); /* flush output */
			cnt++;
		}
	}

	if (cnt == 0)
		rnd_message(RND_MSG_ERROR, "Remote control request failed: can't send, no remote control tcp server connected\n");

	RND_ACT_IRES(0);
	return 0;
}

const char rnd_acts_remctrl_tcp_conn[] = "remctrl_tcp_conn([host,] port)";
const char rnd_acth_remctrl_tcp_conn[] = "create a new remote control connection to the given host:port over tcp; if host is not specified, use localhost";
fgw_error_t rnd_act_remctrl_tcp_conn(fgw_arg_t *res, int argc, fgw_arg_t *argv)
{
	char *host = NULL;
	int port;

	switch(argc) {
		case 3:
			RND_ACT_CONVARG(1, FGW_STR, remctrl_tcp_conn, host = argv[1].val.str);
			RND_ACT_CONVARG(2, FGW_INT, remctrl_tcp_conn, port = argv[2].val.nat_int);
			break;
		case 2:
			RND_ACT_CONVARG(1, FGW_INT, remctrl_tcp_conn, port = argv[1].val.nat_int);
			break;
		default:
			rnd_message(RND_MSG_ERROR, "wrong number of arguments\n");
			return FGW_ERR_ARGC;
	}

	if ((port < 1) || (port > 65535)) {
		rnd_message(RND_MSG_ERROR, "port value is out of range (1..65535): %d\n", port);
		return FGW_ERR_ARGC;
	}

	remctrl_tcp_connect(host, port);
	RND_ACT_IRES(0);
	return 0;
}

const char rnd_acts_remctrl_tcp_del[] = "remctrl_tcp_del([host,] port)";
const char rnd_acth_remctrl_tcp_del[] = "close and remove a remote control connection";
fgw_error_t rnd_act_remctrl_tcp_del(fgw_arg_t *res, int argc, fgw_arg_t *argv)
{
	remconn_t *conn, *nextc;
	char *host = NULL;
	int port, cnt;

	switch(argc) {
		case 3:
			RND_ACT_CONVARG(1, FGW_STR, remctrl_tcp_del, host = argv[1].val.str);
			RND_ACT_CONVARG(2, FGW_INT, remctrl_tcp_del, port = argv[2].val.nat_int);
			break;
		case 2:
			RND_ACT_CONVARG(1, FGW_INT, remctrl_tcp_del, port = argv[1].val.nat_int);
			break;
		default:
			rnd_message(RND_MSG_ERROR, "wrong number of arguments\n");
			return FGW_ERR_ARGC;
	}

	if ((port < 1) || (port > 65535)) {
		rnd_message(RND_MSG_ERROR, "port value is out of range (1..65535): %d\n", port);
		return FGW_ERR_ARGC;
	}


	cnt = 0;
	for(conn = gdl_first(&remconns); conn != NULL; conn = nextc) {
		/* remember next conn because current conn may be removed */
		nextc = gdl_next(&remconns, conn);

		if ((conn->port == port) && conn_host_match(conn, host)) {
			remctrl_tcp_remove(conn);
			cnt++;
		}
	}

	if (cnt == 0) {
		RND_ACT_IRES(1);
		rnd_message(RND_MSG_ERROR, "remctrl_tcp_del(): no such connection\n");
	}
	else {
		RND_ACT_IRES(0);
		rnd_message(RND_MSG_INFO, "remctrl_tcp_del(): closed %d connections\n", cnt);
	}

	return 0;
}

const char rnd_acts_remctrl_tcp_stat[] = "remctrl_tcp_del()";
const char rnd_acth_remctrl_tcp_stat[] = "Print status of remctrl tcp connections to the message log";
fgw_error_t rnd_act_remctrl_tcp_stat(fgw_arg_t *res, int argc, fgw_arg_t *argv)
{
	remconn_t *conn;

	rnd_message(RND_MSG_INFO, "\n\n=== remctrl_tcp status ===\n");
	rnd_message(RND_MSG_INFO, "Timer running: %s\n", timer_running ? "yes" : "no");
	rnd_message(RND_MSG_INFO, "Number of connections: %d\n", remconns.length);
	for(conn = gdl_first(&remconns); conn != NULL; conn = gdl_next(&remconns, conn)) {
		const char *hn = conn->host == NULL ? LOCALHOST : conn->host;
		const char *st;
		switch(conn->st) {
			case RCST_DISCONNECTED:  st = "disconnected"; break;
			case RCST_CONNECTING:    st = "connecting"; break;
			case RCST_NORMAL:        st = "established"; break;
			default:                 st = "UNKNOWN"; break;
		}
		rnd_message(RND_MSG_INFO, " %s:%d - %s\n", hn, conn->port, st);
	}
	rnd_message(RND_MSG_INFO, "\n");
	RND_ACT_IRES(0);
	return 0;
}

/* Returns 0 if no connection is requested, -1 on wrong connection request
   and +1 on connection request accepted */
static int conn_by_env_var(void)
{
	const char *ev = getenv(ENV_VAR);
	char *end;
	long port;

	if ((ev == NULL) || (*ev == '\0'))
		return 0;

	port = strtol(ev, &end, 10);
	if (*end != '\0') {
		rnd_message(RND_MSG_ERROR, "Invalid value of env var " ENV_VAR ": '%s' (must be a decimal integer)\n", ev);
		return -1;
	}
	if ((port < 1) || (port > 65535)) {
		rnd_message(RND_MSG_ERROR, "Invalid value of env var " ENV_VAR ": '%s' (out of range 1..65535)\n", ev);
		return -1;
	}

	remctrl_tcp_connect(NULL, port);
	rnd_message(RND_MSG_INFO, "Remote controlling enabled by env var " ENV_VAR " on tcp port %ld\n", port);
	return +1;
}

static void remctrl_tcp_gui_init_ev(rnd_design_t *hidlib, void *user_data, int argc, rnd_event_arg_t argv[])
{
	conn_by_env_var();
}


rnd_action_t remctrl_tcp_action_list[] = {
	{"remctrl_tcp_req", rnd_act_remctrl_tcp_req, rnd_acth_remctrl_tcp_req, rnd_acts_remctrl_tcp_req},
	{"remctrl_tcp_conn", rnd_act_remctrl_tcp_conn, rnd_acth_remctrl_tcp_conn, rnd_acts_remctrl_tcp_conn},
	{"remctrl_tcp_del", rnd_act_remctrl_tcp_del, rnd_acth_remctrl_tcp_del, rnd_acts_remctrl_tcp_del},
	{"remctrl_tcp_stat", rnd_act_remctrl_tcp_stat, rnd_acth_remctrl_tcp_stat, rnd_acts_remctrl_tcp_stat}
};

static const char *remctrl_tcp_cookie = "remctrl_tcp plugin";

int pplg_check_ver_remctrl_tcp(int ver_needed) { return 0; }

void pplg_uninit_remctrl_tcp(void)
{
	rnd_remove_actions_by_cookie(remctrl_tcp_cookie);
	rnd_event_unbind_allcookie(remctrl_tcp_cookie);
}

int pplg_init_remctrl_tcp(void)
{
	RND_API_CHK_VER;

	RND_REGISTER_ACTIONS(remctrl_tcp_action_list, remctrl_tcp_cookie)

	rnd_event_bind(RND_EVENT_GUI_INIT, remctrl_tcp_gui_init_ev, NULL, remctrl_tcp_cookie);

	return 0;
}
