// ----------------------------------------------------------------------------
// trx.cxx  --  Main transmit/receive control loop / thread
//
// Copyright (C) 2006-2010
//		Dave Freese, W1HKJ
// Copyright (C) 2007-2010
//		Stelios Bounanos, M0GLD
//
// This file is part of fldigi.  Adapted in part from code contained in gmfsk
// source code distribution.
//
// 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 <config.h>

#include <sys/time.h>
#include <fcntl.h>
#include <semaphore.h>
#include <cstdlib>
#include <string>

#include "trx.h"
#include "main.h"
#include "fl_digi.h"
#include "ascii.h"
#include "misc.h"
#include "configuration.h"
#include "status.h"
#include "dtmf.h"

#include "soundconf.h"
#include "ringbuffer.h"
#include "qrunner.h"
#include "debug.h"
#include "nullmodem.h"
#include "macros.h"
#include "rigsupport.h"

#if BENCHMARK_MODE
#  include "benchmark.h"
#endif

LOG_FILE_SOURCE(debug::LOG_MODEM);

using namespace std;

void	trx_reset_loop();
void	trx_start_modem_loop();
void	trx_receive_loop();
void	trx_transmit_loop();
void	trx_tune_loop();
static void trx_signal_state(void);

//#define DEBUG

/* ---------------------------------------------------------------------- */

static sem_t*	trx_sem;
static pthread_t trx_thread;
state_t 	trx_state;

modem		*active_modem = 0;
cRsId		*ReedSolomon = 0;
cDTMF		*dtmf = 0;
SoundBase 	*scard;
static int	_trx_tune;

// Ringbuffer for the audio "history". A pointer into this buffer
// is also passed to the waterfall signal drawing routines.
#define NUMMEMBUFS 1024
static ringbuffer<double> trxrb(ceil2(NUMMEMBUFS * SCBLOCKSIZE));
static float fbuf[SCBLOCKSIZE];
bool	bHistory = false;
bool	bHighSpeed = false;
static  double hsbuff[SCBLOCKSIZE];

static bool trxrunning = false;

bool	rx_only = false;

#include "tune.cxx"

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

// Draws the xmit data one WFBLOCKSIZE-sized block at a time
static void trx_xmit_wfall_draw(int samplerate)
{
	ENSURE_THREAD(FLMAIN_TID);

	ringbuffer<double>::vector_type rv[2];
	rv[0].buf = 0;
	rv[1].buf = 0;

#define block_read_(vec_)						\
	while (vec_.len >= WFBLOCKSIZE) {				\
		wf->sig_data(vec_.buf, WFBLOCKSIZE, samplerate);	\
		vec_.len -= WFBLOCKSIZE;				\
		vec_.buf += WFBLOCKSIZE;				\
		trxrb.read_advance(WFBLOCKSIZE);				\
	}

	trxrb.get_rv(rv);
	block_read_(rv[0]); // read blocks from the first vector

	if (rv[0].len + rv[1].len < WFBLOCKSIZE)
		return;
	if (rv[0].len == 0)
		block_read_(rv[1]);
#undef block_read_

	// read non-contiguous data into tmp buffer so that we can
	// still draw it one block at a time
	if (unlikely(trxrb.read_space() >= WFBLOCKSIZE)) {
		double buf[WFBLOCKSIZE];
		do {
			trxrb.read(buf, WFBLOCKSIZE);
			wf->sig_data(buf, WFBLOCKSIZE, samplerate);
		} while (trxrb.read_space() >= WFBLOCKSIZE);
	}
}

// Called by trx_trx_transmit_loop() to handle data that may be left in the
// ringbuffer when we stop transmitting. Will pad with zeroes to a multiple of
// WFBLOCKSIZE.
static void trx_xmit_wfall_end(int samplerate)
{
	ENSURE_THREAD(TRX_TID);

	size_t pad = WFBLOCKSIZE - trxrb.read_space() % WFBLOCKSIZE;
	if (pad == WFBLOCKSIZE) // rb empty or multiple of WFBLOCKSIZE
		return;

	ringbuffer<double>::vector_type wv[2];
	wv[0].buf = wv[1].buf = 0;

	trxrb.get_wv(wv, pad);
	assert(wv[0].len + wv[1].len == pad);

	if (likely(wv[0].len)) { // fill first vector, write rest to second vector
		memset(wv[0].buf, 0, wv[0].len * sizeof(*wv[0].buf));
		if (pad > wv[0].len)
			memset(wv[1].buf, 0, (pad - wv[0].len) * sizeof(*wv[1].buf));
	}
	else // all write space is in the second write vector
		memset(wv[1].buf, 0, pad * sizeof(*wv[1].buf));

	trxrb.write_advance(pad);

	REQ(trx_xmit_wfall_draw, samplerate);
}

// Copy buf to the ringbuffer if it has enough space. Queue a waterfall
// request whenever there are at least WFBLOCKSIZE samples to draw.
void trx_xmit_wfall_queue(int samplerate, const double* buf, size_t len)
{
	ENSURE_THREAD(TRX_TID);
	ringbuffer<double>::vector_type wv[2];
	wv[0].buf = wv[1].buf = 0;

	trxrb.get_wv(wv, len);
	if (unlikely(wv[0].len + wv[1].len < len)) // not enough space
		return;

#define write_(vec_, len_)					\
	for (size_t i = 0; i < len_; i++)			\
		vec_[i] = buf[i] * progdefaults.TxMonitorLevel;

	size_t n = MIN(wv[0].len, len);
	write_(wv[0].buf, n);
	if (len > n) { // write the rest to the second vector
		buf += n;
		n = len - n;
		write_(wv[1].buf, n);
	}
#undef write_

	trxrb.write_advance(len);
	if (trxrb.read_space() >= WFBLOCKSIZE)
		REQ(trx_xmit_wfall_draw, samplerate);
}

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

void trx_trx_receive_loop()
{
	size_t  numread;
	int  current_samplerate;
	assert(powerof2(SCBLOCKSIZE));

	if (unlikely(!active_modem)) {
	MilliSleep(10);
	return;
	}

#if BENCHMARK_MODE
	do_benchmark();
	trx_state = STATE_ENDED;
	return;
#endif

	if (unlikely(!scard)) {
	MilliSleep(10);
	return;
	}

	try {
		current_samplerate = active_modem->get_samplerate();
		if (scard->Open(O_RDONLY, current_samplerate))
			REQ(sound_update, progdefaults.btnAudioIOis);
	}
	catch (const SndException& e) {
		LOG_ERROR("%s. line: %i", e.what(), __LINE__);
		put_status(e.what(), 5);
		scard->Close();
//	if (e.error() == EBUSY && progdefaults.btnAudioIOis == SND_IDX_PORT) {
		if (progdefaults.btnAudioIOis == SND_IDX_PORT) {
			sound_close();
			sound_init();
		}

		MilliSleep(1000);
		return;
	}
	active_modem->rx_init();

	ringbuffer<double>::vector_type rbvec[2];
	rbvec[0].buf = rbvec[1].buf = 0;

	while (1) {
		try {
			numread = 0;
			while (numread < SCBLOCKSIZE && trx_state == STATE_RX)
				numread += scard->Read(fbuf + numread, SCBLOCKSIZE - numread);
			if (numread > SCBLOCKSIZE) {
				LOG_ERROR("numread error %lu", (unsigned long) numread);
				numread = SCBLOCKSIZE;
			}
			if (bHighSpeed) {
				for (size_t i = 0; i < numread; i++)
					hsbuff[i] = fbuf[i];
			} else {
				if (trxrb.write_space() == 0) // discard some old data
					trxrb.read_advance(SCBLOCKSIZE);
				trxrb.get_wv(rbvec);
			// convert to double and write to rb
				for (size_t i = 0; i < numread; i++)
					rbvec[0].buf[i] = fbuf[i];
			}
		}
		catch (const SndException& e) {
			scard->Close();
			LOG_ERROR("%s. line: %i", e.what(), __LINE__);
			put_status(e.what(), 5);
			MilliSleep(10);
			return;
		}
		if (trx_state != STATE_RX)
			break;

		if (bHighSpeed) {
			bool afc = progStatus.afconoff;
			progStatus.afconoff = false;
			QRUNNER_DROP(true);
			if (progdefaults.rsid)
				ReedSolomon->receive(fbuf, numread);
			active_modem->HistoryON(true);
			active_modem->rx_process(hsbuff, numread);
			QRUNNER_DROP(false);
			progStatus.afconoff = afc;
			active_modem->HistoryON(false);
		} else {
			trxrb.write_advance(numread);
			REQ(&waterfall::sig_data, wf, rbvec[0].buf, numread, current_samplerate);

			if (!bHistory) {
				active_modem->rx_process(rbvec[0].buf, numread);
				if (progdefaults.rsid)
					ReedSolomon->receive(fbuf, numread);
				dtmf->receive(fbuf, numread);
			} else {
				bool afc = progStatus.afconoff;
				progStatus.afconoff = false;
				QRUNNER_DROP(true);
				active_modem->HistoryON(true);
				trxrb.get_rv(rbvec);
				if (rbvec[0].len)
					active_modem->rx_process(rbvec[0].buf, rbvec[0].len);
				if (rbvec[1].len)
					active_modem->rx_process(rbvec[1].buf, rbvec[1].len);
				QRUNNER_DROP(false);
				progStatus.afconoff = afc;
				bHistory = false;
				active_modem->HistoryON(false);
			}
		}
	}
	if (scard->must_close(O_RDONLY))
		scard->Close(O_RDONLY);
}


//=============================================================================
void trx_trx_transmit_loop()
{
	if (rx_only) return;

	int  current_samplerate;
	if (!scard) {
		MilliSleep(10);
		return;
	}
	if (active_modem) {

		try {
			current_samplerate = active_modem->get_samplerate();
			scard->Open(O_WRONLY, current_samplerate);
		}
		catch (const SndException& e) {
			LOG_ERROR("%s. line: %i", e.what(), __LINE__);
			put_status(e.what(), 1);
			MilliSleep(10);
			return;
		}

		if ((active_modem != ssb_modem) &&
			(active_modem != anal_modem) &&
			!active_modem->XMLRPC_CPS_TEST &&
			!PERFORM_CPS_TEST ) {
			push2talk->set(true);
			REQ(&waterfall::set_XmtRcvBtn, wf, true);
		}
		active_modem->tx_init(scard);

		if ( ReedSolomon->assigned(active_modem->get_mode()) &&
			 (progdefaults.TransmitRSid || progStatus.n_rsids != 0)) {
			if (progStatus.n_rsids < 0) {
				for (int i = 0; i > progStatus.n_rsids; i--) {
					ReedSolomon->send(true);
					MilliSleep(200);
				}
			} else if ( progStatus.n_rsids > 0 ) {
				for (int i = 0; i < progStatus.n_rsids; i++) {
					ReedSolomon->send(true);
					MilliSleep(200);
				}
				MilliSleep(200);
				if (progStatus.n_rsids == 1) progStatus.n_rsids = 0;
			} else
				ReedSolomon->send(true);
		}

		if (progStatus.n_rsids >= 0) {

			active_modem->tx_sample_count = 0;
			active_modem->tx_sample_rate = active_modem->get_samplerate();

			while (trx_state == STATE_TX) {
				try {
					if (!progdefaults.DTMFstr.empty())
						dtmf->send();
					if (active_modem->tx_process() < 0)
						if (trx_state != STATE_ABORT)
							trx_state = STATE_RX;
				}
				catch (const SndException& e) {
					scard->Close();
					LOG_ERROR("%s", e.what());
					put_status(e.what(), 5);
					MilliSleep(10);
					return;
				}
			}
		} else
			if (trx_state != STATE_ABORT)
				trx_state = STATE_RX;

		if (ReedSolomon->assigned(active_modem->get_mode()) &&
			progdefaults.TransmitRSid &&
			progdefaults.rsid_post &&
			progStatus.n_rsids >= 0) ReedSolomon->send(false);

		progStatus.n_rsids = 0;

		trx_xmit_wfall_end(current_samplerate);

		scard->flush();
		if (scard->must_close(O_WRONLY))
			scard->Close(O_WRONLY);

	} else
		MilliSleep(10);

	push2talk->set(false);
	REQ(&waterfall::set_XmtRcvBtn, wf, false);
	if (progStatus.timer)
		REQ(startMacroTimer);
	WriteARQ(0x06);
}

//=============================================================================
void trx_tune_loop()
{
	if (rx_only) return;

	int  current_samplerate;
	if (!scard) {
		MilliSleep(10);
		return;
	}
	if (active_modem) {
		try {
			current_samplerate = active_modem->get_samplerate();
			scard->Open(O_WRONLY, current_samplerate);
		}
		catch (const SndException& e) {
			LOG_ERROR("%s. line: %i", e.what(), __LINE__);
			put_status(e.what(), 1);
			MilliSleep(10);
			return;
		}

		push2talk->set(true);
		active_modem->tx_init(scard);

		try {
			while (trx_state == STATE_TUNE) {
				if (_trx_tune == 0) {
					REQ(&waterfall::set_XmtRcvBtn, wf, true);
					xmttune::keydown(active_modem->get_txfreq_woffset(), scard);
					_trx_tune = 1;
				} else
					xmttune::tune(active_modem->get_txfreq_woffset(), scard);
			}
			xmttune::keyup(active_modem->get_txfreq_woffset(), scard);
		}
		catch (const SndException& e) {
			scard->Close();
			LOG_ERROR("%s. line: %i", e.what(), __LINE__);
			put_status(e.what(), 5);
			MilliSleep(10);
			return;
		}
		scard->flush();
		if (scard->must_close(O_WRONLY))
			scard->Close(O_WRONLY);

		_trx_tune = 0;
	} else
		MilliSleep(10);

	push2talk->set(false);
	REQ(&waterfall::set_XmtRcvBtn, wf, false);
}

//=============================================================================
void *trx_loop(void *args)
{
	SET_THREAD_ID(TRX_TID);

	state_t old_state = STATE_NOOP;

	for (;;) {
		if (unlikely(old_state != trx_state)) {
			old_state = trx_state;
			if (trx_state == STATE_TX || trx_state == STATE_TUNE)
				trxrb.reset();
			trx_signal_state();
		}

		LOG_DEBUG("trx state %s",
			trx_state == STATE_ABORT ? "abort" :
			trx_state == STATE_ENDED ? "ended" :
			trx_state == STATE_RESTART ? "restart" :
			trx_state == STATE_NEW_MODEM ? "new modem" :
			trx_state == STATE_TX ? "tx" :
			trx_state == STATE_TUNE ? "tune" :
			trx_state == STATE_RX ? "rx" :
			"unknown");

		switch (trx_state) {
		case STATE_ABORT:
			delete scard;
			scard = 0;
			trx_state = STATE_ENDED;
			// fall through
		case STATE_ENDED:
			REQ(set_flrig_ptt, 0);
			return 0;
		case STATE_RESTART:
			REQ(set_flrig_ptt, 0);
			trx_reset_loop();
			break;
		case STATE_NEW_MODEM:
			REQ(set_flrig_ptt, 0);
			trx_start_modem_loop();
			break;
		case STATE_TX:
			REQ(set_flrig_ptt, 1);
			trx_trx_transmit_loop();
			break;
		case STATE_TUNE:
			REQ(set_flrig_ptt, 1);
			trx_tune_loop();
			break;
		case STATE_RX:
			REQ(set_flrig_ptt, 0);
			trx_trx_receive_loop();
			break;
		default:
			LOG(debug::ERROR_LEVEL, debug::LOG_MODEM, "trx in bad state %d\n", trx_state);
			MilliSleep(100);
		}
	}
}

//=============================================================================
static modem* new_modem;
static int new_freq;

void trx_start_modem_loop()
{
	if (new_modem == active_modem) {
		if (new_freq > 0)
			active_modem->set_freq(new_freq);
		active_modem->restart();
		trx_state = STATE_RX;
		return;
	}

	modem* old_modem = active_modem;

	new_modem->init();
	active_modem = new_modem;
	if (new_freq > 0)
		active_modem->set_freq(new_freq);
	trx_state = STATE_RX;
	REQ(&waterfall::opmode, wf);
	REQ(set599);

	if (old_modem) {
		*mode_info[old_modem->get_mode()].modem = 0;
		delete old_modem;
	}
}

//=============================================================================
void trx_start_modem(modem* m, int f)
{
	new_modem = m;
	new_freq = f;
	trx_state = STATE_NEW_MODEM;
}

//=============================================================================
void trx_reset_loop()
{
	if (scard)  {
		delete scard;
		scard = 0;
	}

	switch (progdefaults.btnAudioIOis) {
#if USE_OSS
	case SND_IDX_OSS:
		scard = new SoundOSS(scDevice[0].c_str());
		break;
#endif
#if USE_PORTAUDIO
	case SND_IDX_PORT:
		scard = new SoundPort(scDevice[0].c_str(), scDevice[1].c_str());
		break;
#endif
#if USE_PULSEAUDIO
	case SND_IDX_PULSE:
		scard = new SoundPulse(scDevice[0].c_str());
		break;
#endif
	case SND_IDX_NULL:
		scard = new SoundNull;
		break;
	default:
		abort();
	}

	trx_state = STATE_RX;
}

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

void trx_reset(void)
{
	trx_state = STATE_RESTART;
}

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

void trx_start(void)
{
#if !BENCHMARK_MODE
	if (trxrunning) {
		LOG(debug::ERROR_LEVEL, debug::LOG_MODEM, "trx already running!");
		return;
	}

	if (scard) delete scard;
	if (ReedSolomon) delete ReedSolomon;
	if (dtmf) delete dtmf;


	switch (progdefaults.btnAudioIOis) {
#if USE_OSS
	case SND_IDX_OSS:
		scard = new SoundOSS(scDevice[0].c_str());
		break;
#endif
#if USE_PORTAUDIO
	case SND_IDX_PORT:
		scard = new SoundPort(scDevice[0].c_str(), scDevice[1].c_str());
		break;
#endif
#if USE_PULSEAUDIO
	case SND_IDX_PULSE:
		scard = new SoundPulse(scDevice[0].c_str());
		break;
#endif
	case SND_IDX_NULL:
		scard = new SoundNull;
		break;
	default:
		abort();
	}

	ReedSolomon = new cRsId;
	dtmf = new cDTMF;

#endif // !BENCHMARK_MODE

#if USE_NAMED_SEMAPHORES
	char sname[32];
	snprintf(sname, sizeof(sname), "trx-%u-%s", getpid(), PACKAGE_TARNAME);
	if ((trx_sem = sem_open(sname, O_CREAT | O_EXCL, 0600, 0)) == (sem_t*)SEM_FAILED) {
		LOG_PERROR("sem_open");
		abort();
	}
#  if HAVE_SEM_UNLINK
	if (sem_unlink(sname) == -1) {
		LOG_PERROR("sem_unlink");
		abort();
	}
#  endif
#else
	trx_sem = new sem_t;
	if (sem_init(trx_sem, 0, 0) == -1) {
		LOG_PERROR("sem_init");
		abort();
	}
#endif

	trx_state = STATE_RX;
	_trx_tune = 0;
	active_modem = 0;
	if (pthread_create(&trx_thread, NULL, trx_loop, NULL) < 0) {
		LOG(debug::ERROR_LEVEL, debug::LOG_MODEM, "pthread_create failed");
		trxrunning = false;
		exit(1);
	}
	trxrunning = true;
}

//=============================================================================
void trx_close()
{
	LOG_INFO("%s", "closing trx thread");
	int count = 1000;
	active_modem->set_stopflag(true);
	while (trx_state != STATE_RX && count--)
		MilliSleep(10);
	if (trx_state != STATE_RX) {
		LOG_INFO("%s", "trx_state != STATE_RX");
		exit(1);
	}
	count = 1000;
	trx_state = STATE_ABORT;
	while (trx_state != STATE_ENDED && count--)
		MilliSleep(10);
	if (trx_state != STATE_ENDED) {
		LOG_INFO("%s", "trx_state != STATE_ENDED");
		exit(2);
	}
#if USE_NAMED_SEMAPHORES
	if (sem_close(trx_sem) == -1)
		LOG_PERROR("sem_close");
#else
	if (sem_destroy(trx_sem) == -1)
		LOG_PERROR("sem_destroy");
	delete trx_sem;
#endif

	if (scard) {
		delete scard;
		scard = 0;
	}
	LOG_INFO("%s", "trx thread closed");
}

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

void trx_transmit(void) { if (!bHAB) trx_state = STATE_TX; }
void trx_tune(void) { trx_state = STATE_TUNE; }
void trx_receive(void) { trx_state = STATE_RX; }

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

void trx_wait_state(void)
{
	ENSURE_NOT_THREAD(TRX_TID);
	sem_wait(trx_sem);
}

static void trx_signal_state(void)
{
	ENSURE_THREAD(TRX_TID);
	sem_post(trx_sem);
}
