// ----------------------------------------------------------------------------
// hydra.cxx
//
// support for HYDRA server/client system
//
// Copyright (C) 2014
//		Steven Passe AD0ES
//
// built with code from:
//	arq_io.cxx
//	xmlrpc.cxx
//
// This file is part of fldigi.
//
// Fldigi 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 3 of the License, or
// (at your option) any later version.
//
// Fldigi 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 fldigi.  If not, see <http://www.gnu.org/licenses/>.
// ----------------------------------------------------------------------------


#include <signal.h>

#include "configuration.h"
#include "fl_digi.h"
#include "status.h"
#include "trx.h"
///#include "xmlrpc.h"

#include "hydra.h"

#include "socket.h"
#include "debug.h"
#include "qrunner.h"

using namespace std;

// =====================================================================
static pthread_t hydra_thread;
static pthread_mutex_t hydra_mutex = PTHREAD_MUTEX_INITIALIZER;
static pthread_mutex_t hydra_rx_mutex = PTHREAD_MUTEX_INITIALIZER;

static void *hydra_transmitter(void *args);
static void *hydra_receiver(void *args);

static bool hydra_exit = false;
static bool hydra_enabled;

static string datastring = ""; // Protected by hydra_rx_mutex

#define CLIENT_TIMEOUT 5 // timeout after N secs

struct HYDRACLIENT { Socket sock; time_t keep_alive; };
static string errstring;

static HYDRACLIENT *currentClient = 0;
static pthread_t* hydra_socket_thread = 0;
HYDRA_SOCKET_Server* HYDRA_SOCKET_Server::inst = 0;

void hydra_run(Socket);

HYDRA_SOCKET_Server::HYDRA_SOCKET_Server()
{
	server_socket = new Socket;
	hydra_socket_thread = new pthread_t;
	run = true;
}

HYDRA_SOCKET_Server::~HYDRA_SOCKET_Server()
{
	run = false;
	if (hydra_socket_thread) {
		CANCEL_THREAD(*hydra_socket_thread);
		pthread_join(*hydra_socket_thread, NULL);
		delete hydra_socket_thread;
		hydra_socket_thread = 0;
	}
	delete server_socket;
}

bool HYDRA_SOCKET_Server::start(const char* node, const char* service)
{
	if (inst) return false;

	inst = new HYDRA_SOCKET_Server;

	try {
		inst->server_socket->open(Address(node, service));
		inst->server_socket->bind();
	}
	catch (const SocketException& e) {
		errstring.assign("Could not start HYDRA server (");
		errstring.append(e.what()).append(")");
		if (e.error() == EADDRINUSE)
			errstring.append("\nMultiple instances of fldigi??");
		LOG_ERROR("%s", errstring.c_str());

		delete hydra_socket_thread;
		hydra_socket_thread = 0;
		delete inst;
		inst = 0;
		return false;
	}

	return !pthread_create(hydra_socket_thread, NULL, thread_func, NULL);
}

void HYDRA_SOCKET_Server::stop(void)
{
	// FILEME - uncomment when we have an HYDRA_SOCKET_Server than can be
	// interrupted
	//	if (!inst)
	//		return;
	//	delete inst;
	//	inst = 0;
}

void* HYDRA_SOCKET_Server::thread_func(void*)
{
	SET_THREAD_ID(HYDRASOCKET_TID);
	SET_THREAD_CANCEL();

	// On POSIX we block indefinitely and are interrupted by a signal.
	while (inst->run) {
		try {
#if 0
			if (inst->server_socket->wait(0)) {
				hydra_run(inst->server_socket->accept());
			}
#else
			// I totally mis-understood the code in misc/arq_io.cxx...
			// This is what we want for POSIX (I think)
			hydra_run(inst->server_socket->accept());
			TEST_THREAD_CANCEL();
#endif
		}
		catch (const SocketException& e) {
			if (e.error() != EINTR) {
				errstring = e.what();
				LOG_ERROR("%s", errstring.c_str());
				break;
			}
		}
		catch (...) {
			break;
		}
	}

	{
		// Mutex is unlocked when leaving the block.
		guard_lock hydra_lock(&hydra_mutex);

		if (currentClient) {
			currentClient->sock.close();
			currentClient->sock.shut_down();
			delete currentClient;
			currentClient = 0;
		}
	}

	inst->server_socket->close();
	return NULL;
}

void hydra_reset()
{
	// Mutex is unlocked when returning from function.
	guard_lock hydra_rx_lock(&hydra_rx_mutex);

	datastring.clear();
}


static syncobj m_sync_rx;

void hydra_run(Socket s)
{
	{
		// Mutex is unlocked when leaving block.
		guard_lock hydra_lock(&hydra_mutex);

		// We need to close the existing client socket.
		if (currentClient) {
			currentClient->sock.close();
			currentClient->sock.shut_down();
			delete currentClient;
			currentClient = 0;
/*
from man select:
 Multithreaded applications
       If a file descriptor being monitored by select() is closed in another thread, the result is  unspeci‐
       fied.  On some UNIX systems, select() unblocks and returns, with an indication that the file descrip‐
       tor is ready (a subsequent I/O operation will likely fail with an  error,  unless  another  the  file
       descriptor  reopened  between  the  time select() returned and the I/O operations was performed).  On
       Linux (and some other systems), closing the file descriptor  in  another  thread  has  no  effect  on
       select().   In summary, any application that relies on a particular behavior in this scenario must be
       considered buggy.
*/
			// We send a signal to terminate any pending select().
			if (int r = pthread_kill(hydra_thread, SIGUSR1))
				LOG_ERROR("unsuccessful kill: %d\n", r);
		}

		// Create a new client socket.
		struct timeval t = { 0, 20000 };
		s.set_timeout(t);
		s.set_nonblocking();
		currentClient = new HYDRACLIENT;
		currentClient->sock = s;
	}

	// Signal the arrival of a client.
	guard_lock g( m_sync_rx.mtxp() );
	m_sync_rx.signal();
}

// Wait for data, send keep-alive bytes to the client.
int hydra_wait(int sockfd)
{
	fd_set fdsetIn;
	fd_set fdsetEx;
	FD_ZERO(&fdsetIn);
	FD_SET(sockfd, &fdsetIn);
	FD_ZERO(&fdsetEx);
	FD_SET(sockfd, &fdsetEx);

	struct timeval t = { CLIENT_TIMEOUT, 0 };
	try {
		int r = select(sockfd + 1, &fdsetIn, NULL, &fdsetEx, &t);

		if (r == 0) {
			// Timeout, send keep-alive byte.
			guard_lock hydra_lock(&hydra_mutex);	// Unlocked when when leaving block.
			currentClient->sock.wait(1);
			currentClient->sock.send("\0", 1);
			return (0);
		}

		if (r == -1) {
			// Early termination of select() via SIGUSR1
			if (errno == EINTR)
				return (0);

			if (currentClient) {
				guard_lock hydra_lock(&hydra_mutex);	// Unlocked when when leaving block.
				currentClient->sock.close();
				currentClient->sock.shut_down();
				delete currentClient;
				currentClient = 0;
			}

			perror("hydra_wait, select()");
			return (-1);
		}

		// Unexpected result of some sort.
		if (FD_ISSET(sockfd, &fdsetEx)) {
			LOG_INFO(">>> EXCEPTION");
			throw SocketException(errno, "hydra_wait");
			return (-1);
		}
		// Normal path with available data.
		if (FD_ISSET(sockfd, &fdsetIn)) {
	  		return (1);
		}
	}
	catch (const SocketException& e) {
		LOG_INFO("selection fd %d, %d: %s", currentClient->sock.fd(), e.error(), e.what());
	}

	return (-1);
}

// REQ(set_combo_value, qso_opMODE, mode); causes my K3 to change to xfilt 5 for some strange reason...
// I believe the basic problem is that opModes (USB/LSB/etc.) are more or less unique between rigs.
// One may use 'DATA' while another uses 'PKTUSB' while yet another uses 'DIGU'.  And these are just
// textual representations, 'DIGU' might be index 7 on rig A, while its "equilivant" 'PKTUSB'
// might be index 5 on rig B.  So till I can figure out a not-too-complicated mapping scheme we
// live without...
#define SET_MODE	0

// Not sure if this helps anything...
#define SET_BW		1
#if SET_MODE
static void set_combo_value(Fl_ComboBox* box, const string& s)
{
	box->value(s.c_str());
	box->do_callback();
}
#endif // SET_MODE

#include "rigsupport.h"

// XXX see arq_io.cxx for additional subcmd patterns.

/*
 <!-- type: int; default: 2
Number of tones. Values are as follows:
  0: 2; 1: 4; 2: 8; 3: 16; 4: 32; 5: 64; 6: 128; 7: 256 -->

<!-- type: int; default: 2
Bandwidth (Hz). Values are as follows:
  0: 125; 1: 250; 2: 500; 3: 1000; 4: 2000. -->
*/				      //
static void do_contestia(int tones, int bw)
{
	LOG_INFO("tones: %d, bw: %d\n", tones, bw);

	if (tones < 1) tones = 2;
	if (bw < 1) bw = 1;

	progdefaults.contestiatones = tones;
	progdefaults.contestiabw = bw;
}

static void do_rtty(void)
{
	LOG_ERROR("CAUTION: hydra RTTY not fully supported!");
}

void parse_hydratext(string &toparse)
{
	static	string strCmdText;
	static	string strSubCmd;
	unsigned long int idxCmd, idxCmdEnd, idxSubCmd, idxSubCmdEnd;

	static long modem;
	static long long freq;
	static int offset;
#if SET_MODE
	static string mode;
#endif // SET_MODE
	static float bandwidth;
	static string call;

	static int ct_tones = -1;
	static int ct_bw = -1;

	if (toparse.empty()) return;

	idxCmd = toparse.find("<cmd>");
	idxCmdEnd = toparse.find("</cmd>");

	while (idxCmd != string::npos && idxCmdEnd != string::npos && idxCmdEnd > idxCmd) {
		///LOG_INFO("Parsing: %s", toparse.substr(idxCmd, idxCmdEnd - idxCmd + 6).c_str());

		strCmdText = toparse.substr(idxCmd + 5, idxCmdEnd - idxCmd - 5);

		if ((idxSubCmd = strCmdText.find("<freq>")) != string::npos) {
			idxSubCmdEnd = strCmdText.find("</freq>");
			if (idxSubCmdEnd != string::npos &&
			    idxSubCmdEnd > idxSubCmd) {
				strSubCmd = strCmdText.substr(idxSubCmd + 6, idxSubCmdEnd - idxSubCmd - 6);
				freq = atoll(strSubCmd.c_str());
				///LOG_INFO("hydra frequency: '%lld'", freq);
			}
		} else if ((idxSubCmd = strCmdText.find("<offset>")) != string::npos) {
			idxSubCmdEnd = strCmdText.find("</offset>");
			if (idxSubCmdEnd != string::npos &&
			    idxSubCmdEnd > idxSubCmd) {
				strSubCmd = strCmdText.substr(idxSubCmd + 8, idxSubCmdEnd - idxSubCmd - 8);
				offset = atoi(strSubCmd.c_str());
				///LOG_INFO("hydra offset: '%s'", strSubCmd.c_str());
			}
#if SET_MODE
		} else if ((idxSubCmd = strCmdText.find("<mode>")) != string::npos) {
			idxSubCmdEnd = strCmdText.find("</mode>");
			if (idxSubCmdEnd != string::npos &&
			    idxSubCmdEnd > idxSubCmd) {
				strSubCmd = strCmdText.substr(idxSubCmd + 6, idxSubCmdEnd - idxSubCmd - 6);
				mode = strSubCmd;
				///LOG_INFO("hydra mode: '%s'", mode.c_str());
			}
#endif // SET_MODE
		} else if ((idxSubCmd = strCmdText.find("<modem>")) != string::npos) {
			idxSubCmdEnd = strCmdText.find("</modem>");
			if (idxSubCmdEnd != string::npos &&
			    idxSubCmdEnd > idxSubCmd) {
				strSubCmd = strCmdText.substr(idxSubCmd + 7, idxSubCmdEnd - idxSubCmd - 7);
				modem = atoi(strSubCmd.c_str());
				///LOG_INFO("hydra modem: '%ld'", modem);
			}
		} else if ((idxSubCmd = strCmdText.find("<bw>")) != string::npos) {
			idxSubCmdEnd = strCmdText.find("</bw>");
			if (idxSubCmdEnd != string::npos &&
			    idxSubCmdEnd > idxSubCmd) {
				strSubCmd = strCmdText.substr(idxSubCmd + 4, idxSubCmdEnd - idxSubCmd - 4);
				bandwidth = atof(strSubCmd.c_str());
				///LOG_INFO("hydra bw: '%f'", bandwidth);
			}
		} else if ((idxSubCmd = strCmdText.find("<ct_tones>")) != string::npos) {
			idxSubCmdEnd = strCmdText.find("</ct_tones>");
			if (idxSubCmdEnd != string::npos &&
			    idxSubCmdEnd > idxSubCmd) {
				strSubCmd = strCmdText.substr(idxSubCmd + 10, idxSubCmdEnd - idxSubCmd - 10);
				ct_tones = atoi(strSubCmd.c_str());
				///LOG_INFO("hydra ct_tones: '%d'", ct_tones);
			}
		} else if ((idxSubCmd = strCmdText.find("<ct_bw>")) != string::npos) {
			idxSubCmdEnd = strCmdText.find("</ct_bw>");
			if (idxSubCmdEnd != string::npos &&
			    idxSubCmdEnd > idxSubCmd) {
				strSubCmd = strCmdText.substr(idxSubCmd + 7, idxSubCmdEnd - idxSubCmd - 7);
				ct_bw = atoi(strSubCmd.c_str());
				///LOG_INFO("hydra bw: '%d'", ct_bw);
			}
		} else if ((idxSubCmd = strCmdText.find("<call>")) != string::npos) {
			idxSubCmdEnd = strCmdText.find("</call>");
			if (idxSubCmdEnd != string::npos &&
			    idxSubCmdEnd > idxSubCmd) {
				strSubCmd = strCmdText.substr(idxSubCmd + 6, idxSubCmdEnd - idxSubCmd - 6);
				call = strSubCmd;
				///LOG_INFO("hydra callsign: '%s'", call.c_str());
			}
		}

		toparse.erase(idxCmd, idxCmdEnd - idxCmd + 6);
		while (toparse[0] == '\n' || toparse[0] == '\r') toparse.erase(0, 1);

		idxCmd = toparse.find("<cmd>");
		idxCmdEnd = toparse.find("</cmd>");
	}

	toparse.clear();

	// This is necessary to set bandwidth for contestia and possibly: rtty ???
	if (modem == MODE_CONTESTIA) {
		do_contestia(ct_tones, ct_bw);
	}
	else if (modem == MODE_RTTY) {
		do_rtty();
	}

	// Use the meta-data to setrup the qso.
	REQ_SYNC(init_modem_sync, modem, 0);		// set the modem, ie. bpsk, olivia, etc.

	if (offset == 0) {
		// Adjust freq + offset for sweetspot.
		freq -= progdefaults.PSKsweetspot;
		offset = progdefaults.PSKsweetspot;
	}

	// Set the dial frequency and waterfall offset.
	active_modem->set_freq(offset);			// set the waterfall offset from dial frequency
	qsy(freq);					// set the dial frequency

#if SET_BW
	// Is it necessary to set bandwidth for olivia?
	active_modem->set_bandwidth(bandwidth);		// necessary for ???
#endif // SET_BW

#if SET_MODE
	REQ(set_combo_value, qso_opMODE, mode);		// set the mode, ie. DIGU/USB/etc.
#endif // SET_MODE

	// Set the callsign value for macros.
	inpCall->value(call.c_str());
}

bool Socket_hydraRx()
{
	{
		// Mutex is unlocked when leaving block.
		guard_lock hydra_lock(&hydra_mutex);

		if (!currentClient) return (false);

		static string instr;
		size_t n = 0;
		instr.clear();

		try {
			n = currentClient->sock.recv(instr);
			if ( n > 0) {
				datastring.append(instr);
			}
			else if (n == 0) {
				throw SocketException(errno, "closed socket?");
			}
		}
		catch (const SocketException& e) {
			datastring.clear();
			LOG_INFO("closing socket fd %d, %d: %s", currentClient->sock.fd(), e.error(), e.what());
			try {
				currentClient->sock.close();
			}
			catch (const SocketException& e) {
				LOG_ERROR("socket error on # %d, %d: %s", currentClient->sock.fd(), e.error(), e.what());
			}
		}
	}

	if (!datastring.empty()) parse_hydratext(datastring);

	return (true);
}

// This function catches SIGUSR1, used to terminate pending select() call in hydra_wait().
static void catchUSR1(int i)
{
	return;	// nothing to do, our occurrence is sufficient to terminate any pending select()
}

// The xmitter's client connection thread.
static void *hydra_transmitter(void *args)
{
	SET_THREAD_ID(HYDRACLIENT_TID);
	SET_THREAD_CANCEL();

	// Arrange for SIGUSR1 to terminate pending select().
	signal(SIGUSR1, catchUSR1);

	// Main loop.
	for (;;) {

		if (!currentClient) {
			{
				guard_lock g( m_sync_rx.mtxp() );
				while (!m_sync_rx.wait(CLIENT_TIMEOUT)) {
					if (hydra_exit)
						return (NULL);
				}
			}
		}

		if (hydra_exit)
			return (NULL);

		int r = hydra_wait(currentClient->sock.fd());
		if (r > 0)
			Socket_hydraRx();	// read data
	}

	// Exit the hydra thread.
	return NULL;
}

// The receiver's connection thread.
static void *hydra_receiver(void *args)
{
	SET_THREAD_ID(HYDRACLIENT_TID);
	SET_THREAD_CANCEL();

	for (;;) {

		if (hydra_exit)
			return (NULL);

		// XXX 
		sleep(30);
		///LOG_INFO("helo");
	}

	// Exit the hydra thread.
	return NULL;
}

void hydra_xmit_init(int unit)
{
	LOG_INFO("starting unit: %d", unit);

	datastring.clear();

	char ps[20];
	sprintf(ps, "%d", atoi(progdefaults.hydra_port.c_str()) + unit);
	if (HYDRA_SOCKET_Server::start(progdefaults.hydra_address.c_str(), ps)) {
		fprintf(stderr,
			"####################################################> starting Hydra:\n"
			"dsp: %d, hydra server: %s hydra port: %s hydra role: %s\n"
			"#####################################################################\n",
			progdefaults.SdrReceiver, progdefaults.hydra_address.c_str(),
			ps, progdefaults.hydra_role.c_str());
	}
	else {
		LOG_ERROR("socket server start failed");
		return;
	}

	// Start the client loop.
	if (pthread_create(&hydra_thread, NULL, hydra_transmitter, NULL) < 0) {
		LOG_ERROR("hydra_transmitter creation failed");
		return;
	}

	hydra_enabled = true;
}

void hydra_rcv_init(void)
{
	LOG_INFO("start");

	// Start the reading loop.
	if (pthread_create(&hydra_thread, NULL, hydra_receiver, NULL) < 0) {
		LOG_ERROR("hydra_receiver creation failed");
		return;
	}

	hydra_enabled = true;
}

void hydra_init(void)
{
	int unit = 0;
	hydra_enabled = false;

	if (strncmp(progdefaults.hydra_role.c_str(), "none", 4) == 0) {
		LOG_INFO("hydra OFF");
		return;
	}
	else if (strncmp(progdefaults.hydra_role.c_str(), "transmit", 8) == 0) {
		if (progdefaults.hydra_role[8] == ':')
			unit = atoi(&(progdefaults.hydra_role[9]));
		return (hydra_xmit_init(unit));
	}
	else if (strncmp(progdefaults.hydra_role.c_str(), "receive", 7) == 0) {
		return (hydra_rcv_init());
	}
	else {
		LOG_ERROR("hydra misconfigured: %s %s %s",
			progdefaults.hydra_role.c_str(),
			progdefaults.hydra_address.c_str(),
			progdefaults.hydra_port.c_str());
	}
}

void hydra_close(void)
{
	if (!hydra_enabled) return;

	HYDRA_SOCKET_Server::stop();

	// Tell the hydra thread to kill itself,
	hydra_exit = true;

	CANCEL_THREAD(hydra_thread);
	hydra_enabled = false;

	hydra_exit = false;

	LOG_INFO("hydra_close()");
}

void hydra_metadata(const char *s, bool sweetspot)
{
	Socket	*hydra_tcpip = 0;

	try {
		// create a socket
		hydra_tcpip = new Socket(Address(progdefaults.hydra_address.c_str(), s));
		hydra_tcpip->set_timeout(0.01);
		hydra_tcpip->connect();
	}
	catch (...) {
		LOG_ERROR(">>> DSP server @ ip address %s, port %s is not available.",
			  progdefaults.hydra_address.c_str(), progdefaults.hydra_port.c_str());
		LOG_ERROR(">>>   Hardware must be powered & connected.");
		LOG_ERROR(">>>   Alex hpsdr server and dspserver must be running.");
		LOG_ERROR(">>>   If ip addr: '%s' is incorrect,\n"
			  "\t\tedit [config-dir]/fldigi_def.xml:\n"
			  "\t\t<SDRDSPHOST>[correct ip addr here]</SDRDSPHOST>",
			  progdefaults.hydra_address.c_str());
	}
#if 0
	LOG_INFO("modem #: %ld, modem name: %s, freq: %ld, bw: %f",
		 active_modem->get_mode(), active_modem->get_mode_name(),
		 qsoFreqDisp->value(),
		 active_modem->get_bandwidth()
	);
#endif
	char str[800] = { '\0' };
	char str2[200] = { '\0' };

#if SET_MODE
#else
	// Note: although we are sending the opMode value we don't use it at the transmitter.
#endif // SET_MODE

	long int f = qsoFreqDisp->value();
	int o = active_modem->get_freq();
	if (sweetspot) {
		f += o;
		o = 0;
	}

	// Base meta-data necessary for all hydra transactions.
	sprintf(str,
		"<cmd><modem>%ld</modem></cmd>"
		"<cmd><freq>%ld</freq></cmd>"
		"<cmd><offset>%d</offset></cmd>"
		"<cmd><mode>%s</mode></cmd>"
		"<cmd><bw>%f</bw></cmd>"
		"<cmd><call>%s</call></cmd>",
		active_modem->get_mode(),		// bpsk, olivia, etc.
		f,					// dial frequency
		o,					// offset from dial frequency
		qso_opMODE->value(),			// DIGU/USB/etc.
		active_modem->get_bandwidth(),
		inpCall->value()
	);		// bandwidth of signal

	// Additional meta-data necessary for contestia.
	if (active_modem->get_mode() == MODE_CONTESTIA) {
		sprintf(str2,
			"<cmd><ct_tones>%d</ct_tones></cmd>"
			"<cmd><ct_bw>%d</ct_bw></cmd>",
			progdefaults.contestiatones,
			progdefaults.contestiabw);
		strncat(str, str2, 200);
	}
	// Additional meta-data necessary for rtty.
	else if (active_modem->get_mode() == MODE_RTTY) {
		// XXX what to do?
	}

	hydra_tcpip->send(str);
}

//======================================================================
// 
//======================================================================
