/**
 * radiotest.cpp
 * This file is part of the YATE Project http://YATE.null.ro
 *
 * Radio interface test
 *
 * Yet Another Telephony Engine - a fully featured software PBX and IVR
 * Copyright (C) 2015 Null Team
 * Copyright (C) 2015 LEGBA Inc
 *
 * This software is distributed under multiple licenses;
 * see the COPYING file in the main directory for licensing
 * information for this specific distribution.
 *
 * This use of this software may be subject to additional restrictions.
 * See the LEGAL file in the main directory for details.
 *
 * 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.
 */

#include <yatephone.h>
#include <yateradio.h>
#include <yatemath.h>

using namespace TelEngine;
namespace { // anonymous

class RadioTestRecv;
class RadioTestModule;

class RadioTestIO
{
public:
    inline RadioTestIO(bool _tx)
	: tx(_tx), enabled(false), startTime(0), ts(0), transferred(0)
	{}
    bool tx;
    bool enabled;
    uint64_t startTime;
    uint64_t ts;
    uint64_t transferred;
};

class RadioTest : public Thread, public DebugEnabler
{
    friend class RadioTestRecv;
    friend class RadioTestModule;
public:
    RadioTest(const NamedList& params, const NamedList& radioParams);
    ~RadioTest()
	{ terminated(m_started ? "destroyed" : 0); }
    bool command(const String& cmd, const NamedList& params);
    static bool start(const NamedList& params, const NamedList& radioParams);

protected:
    virtual void cleanup()
	{ terminated(m_started ? "cleanup" : 0); }
    virtual void run();
    void terminated(const char* error);
    void initPulse();
    bool setTxData();
    void regenerateTxData();
    bool execute(const String& cmd, const String& param, bool fatal,
	const NamedList* params);
    bool execute(const NamedList& cmds, const char* prefix);
    bool write();
    bool read();
    void readTerminated(RadioTestRecv* th);
    void readStop();
    void hardCancelRecv();
    inline void updateTs(bool tx) {
	    uint64_t ts = 0;
	    if ((tx ? m_radio->getTxTime(ts) : m_radio->getRxTime(ts)) == 0) {
		(tx ? m_tx.ts : m_rx.ts) = ts;
		Debug(this,DebugInfo,"Updated %s ts=" FMT64U " [%p]",
		    (tx ? "TX" : "RX"),ts,this);
	    }
	}
    bool wait(const String& param);
    void notify(const char* state, const char* error = 0, const NamedList* params = 0) const;

    RadioInterface* m_radio;
    RadioTestRecv* m_recv;
    bool m_started;
    unsigned int m_repeat;
    NamedList m_init;
    NamedList m_params;
    NamedList m_radioParams;
    // TX
    RadioTestIO m_tx;
    bool m_txYield;
    bool m_newTxData;
    unsigned int m_phase;
    unsigned int m_sendBufCount;
    ComplexVector m_sendBufData;
    // Pulse
    unsigned int m_pulse;
    ComplexVector m_pulseData;
    // RX
    RadioTestIO m_rx;
    RadioReadBufs m_bufs;
    uint64_t m_rxSkippedSamples;
    DataBlock m_crt;
    DataBlock m_aux;
    DataBlock m_extra;
};

class RadioTestRecv : public Thread
{
public:
    inline RadioTestRecv(RadioTest* test, Thread::Priority prio)
	: Thread("RadioTestRecv",prio), m_test(test)
	{}
    ~RadioTestRecv()
	{ notify(); }
    void cleanup()
	{ notify(); }
    static inline RadioTestRecv* start(RadioTest* test, Thread::Priority prio) {
	    RadioTestRecv* tmp = new RadioTestRecv(test,prio);
	    if (tmp->startup())
		return tmp;
	    delete tmp;
	    return 0;
	}
protected:
    virtual void run() {
	    if (!m_test)
		return;
	    while (!Thread::check(false) && m_test->read())
		;
	    notify();
	}
    inline void notify() {
	    RadioTest* tmp = m_test;
	    m_test = 0;
	    if (tmp)
		tmp->readTerminated(this);
	}
    RadioTest* m_test;
};

class RadioTestModule : public Module
{
public:
    RadioTestModule();
    ~RadioTestModule();

protected:
    virtual void initialize();
    virtual bool received(Message& msg, int id);
    virtual bool commandComplete(Message& msg, const String& partLine,
	const String& partWord);
    bool onCmdControl(Message& msg);
    bool test(const String& cmd = String::empty(),
	const NamedList& params = NamedList::empty());
    void processRadioDataFile(NamedList& params);
};

INIT_PLUGIN(RadioTestModule);
static RadioTest* s_test = 0;
static Mutex s_testMutex(false,"RadioTest");

static inline unsigned int threadIdleIntervals(unsigned int ms)
{
    return (ms + Thread::idleMsec()) / Thread::idleMsec();
}

static inline bool validFloatSample(float val)
{
    return (val >= -1.0F) && (val <= 1.0F);
}

static inline const char* encloseDashes(String& s, bool extra = false)
{
    static const String s1 = "\r\n-----";
    if (s)
	s = s1 + (extra ? "\r\n" : "") + s + s1;
    return s.safe();
}

static inline unsigned int samplesf2bytes(unsigned int samples)
{
    return samples * 2 * sizeof(float);
}

static String& dumpSamplesFloat(String& s, const DataBlock& buf,
    const char* fmt4, const char* fmt, const char* sep, unsigned int maxDump = 0)
{
    unsigned int samples = buf.length() / (2 * sizeof(float));
    const float* f = (const float*)buf.data();
    if (!(f && samples))
	return s;
    if (maxDump && maxDump < samples)
	samples = maxDump;
    String tmp;
    unsigned int a = samples / 4;
    for (; a; a--, f += 8)
	s.append(tmp.printf(512,fmt4,f[0],f[1],f[2],f[3],f[4],f[5],f[6],f[7]),sep);
    for (a = samples % 4; a; a--, f += 2)
	s.append(tmp.printf(fmt,f[0],f[1]),sep);
    return s;
}

static String& dumpSamplesInt16(String& s, const DataBlock& buf,
    const char* fmt4, const char* fmt, const char* sep, unsigned int maxDump = 0)
{
    unsigned int samples = buf.length() / (2 * sizeof(int16_t));
    const int16_t* f = (const int16_t*)buf.data();
    if (!(f && samples))
	return s;
    if (maxDump && maxDump < samples)
	samples = maxDump;
    String tmp;
    unsigned int a = samples / 4;
    for (; a; a--, f += 8)
	s.append(tmp.printf(512,fmt4,f[0],f[1],f[2],f[3],f[4],f[5],f[6],f[7]),sep);
    for (a = samples % 4; a; a--, f += 2)
	s.append(tmp.printf(fmt,f[0],f[1]),sep);
    return s;
}

static inline bool boolSetError(String& s, const char* e = 0)
{
    s = e;
    return false;
}

// Parse a comma separated list of float values to complex vector
static bool parseVector(String& error, const String& str, ComplexVector& buf)
{
    if (!str)
	return boolSetError(error,"empty");
    ObjList* list = str.split(',');
    unsigned int len = list->length();
    if ((len < 2) || (len % 2) != 0)
	return boolSetError(error,"invalid length");
    buf.resetStorage(len);
    ObjList* o = list;
    for (float* b = (float*)buf.data(); o; o = o->next(), b++) {
	if (!o->get())
	    continue;
	*b = (static_cast<String*>(o->get()))->toDouble();
	if (!validFloatSample(*b))
	    break;
    }
    TelEngine::destruct(list);
    return (o == 0) ? true : boolSetError(error,"invalid data range");
}

static inline void generateCircleQuarter(Complex*& c, float amplitude, float i, float q,
    unsigned int loops, float angle, float iSign, float qSign)
{
    (c++)->set(i * amplitude,q * amplitude);
    if (!loops)
	return;
    float angleStep = M_PI_2 / (loops + 1);
    if (angle)
	angleStep = -angleStep;
    iSign *= amplitude;
    qSign *= amplitude;
    for (; loops; --loops, ++c) {
	angle += angleStep;
	c->set(iSign * ::cosf(angle),qSign * ::sinf(angle));
    }
}

// Parse a complex numbers pattern
// forcePeriodic=true: Force lenExtend=false and lenRequired=true for periodic
//                     patterns (like 'circle')
// lenExtend=true: Extend destination buffer to be minimum 'len'. 'lenRequired' is ignored
// lenRequired=true: 'len' MUST be a multiple of generated vector's length
static bool buildVector(String& error, const String& pattern, ComplexVector& vector,
    unsigned int len = 0, bool forcePeriodic = true, bool lenExtend = true,
    bool lenRequired = false, unsigned int* pLen = 0)
{
    if (!pattern)
	return boolSetError(error,"empty");
    bool isPeriodic = false;
    String p = pattern;
    ComplexVector v;
    // Check for circles
    if (p.startSkip("circle",false)) {
	unsigned int cLen = 4;
	bool rev = false;
	float div = 1;
	if (!p || p == YSTRING("_reverse"))
	    // circle[_reverse]
	    rev = !p.null();
	else if (p.startSkip("_div_",false)) {
	    // circle_div[_reverse]_{divisor}
	    rev = p.startSkip("reverse_",false);
	    if (!p)
		return boolSetError(error);
	    div = p.toDouble();
	}
	else if (p.startSkip("_points_",false)) {
	    // circle_points[_reverse]_{value}[_div_{divisor}]
	    rev = p.startSkip("reverse_",false);
	    if (!p)
		return boolSetError(error);
	    int pos = p.find('_');
	    if (pos < 0)
		cLen = p.toInteger(0,0,0);
	    else {
		// Expecting div
		cLen = p.substr(0,pos).toInteger(0,0,0);
		p = p.substr(pos + 1);
		if (!(p.startSkip("div_",false) && p))
		    return boolSetError(error);
		div = p.toDouble();
	    }
	}
	else
	    return boolSetError(error);
	// Circle length MUST be a multiple of 4
	if (!cLen || (cLen % 4) != 0)
	    return boolSetError(error,"invalid circle length");
	if (div < 1)
	    return boolSetError(error,"invalid circle div");
	v.resetStorage(cLen);
	Complex* c = v.data();
	float amplitude = 1.0F / div;
	float direction = rev ? -1 : 1;
	unsigned int n = (cLen - 4) / 4;
	generateCircleQuarter(c,amplitude,1,0,n,0,1,direction);
	generateCircleQuarter(c,amplitude,0,direction,n,M_PI_2,-1,direction);
	generateCircleQuarter(c,amplitude,-1,0,n,0,-1,-direction);
	generateCircleQuarter(c,amplitude,0,-direction,n,M_PI_2,1,-direction);
	isPeriodic = true;
    }
    else if (pattern == YSTRING("zero")) {
	// Fill with 0
	vector.resetStorage(len ? len : 1);
	if (pLen)
	    *pLen = 1;
	return true;
    }
    else if (p.startSkip("fill_",false)) {
	// Fill with value: fill_{real}_{imag}
	int pos = p.find('_');
	if (pos < 1 || p.find('_',pos + 1) > 0)
	    return boolSetError(error);
	float re = p.substr(0,pos).toDouble();
	float im = p.substr(pos + 1).toDouble();
	if (validFloatSample(re) && validFloatSample(im)) {
	    vector.resetStorage(len ? len : 1);
	    vector.fill(Complex(re,im));
	    if (pLen)
		*pLen = 1;
	    return true;
	}
	return boolSetError(error,"invalid data range");
    }
    else if (!parseVector(error,pattern,v))
	// Parse list of values
	return false;
    if (!v.length())
	return boolSetError(error,"empty result");
    if (pLen)
	*pLen = v.length();
    if (isPeriodic && forcePeriodic) {
	lenExtend = false;
	lenRequired = true;
    }
    // Try to extend data
    if (!len || (len == v.length()) || !(lenExtend || lenRequired))
	vector = v;
    else {
	if (lenExtend) {
	    if (len < v.length())
		len = v.length();
	    unsigned int rest = len % v.length();
	    if (rest)
		len += v.length() - rest;
	}
	else if ((len < v.length()) || ((len % v.length()) != 0))
	    return boolSetError(error,"required/actual length mismatch");
	vector.resetStorage(len);
	for (unsigned int i = 0; (i + v.length()) <= len; i += v.length())
	    vector.slice(i,v.length()).copy(v,v.length());
    }
    return true;
}


//
// RadioTest
//
RadioTest::RadioTest(const NamedList& params, const NamedList& radioParams)
    : Thread("RadioTest",Thread::priority(params["priority"])),
    m_radio(0),
    m_recv(0),
    m_started(false),
    m_repeat(0),
    m_init(""),
    m_params(params),
    m_radioParams(radioParams),
    m_tx(true),
    m_txYield(true),
    m_newTxData(false),
    m_phase(0),
    m_sendBufCount(0),
    m_pulse(0),
    m_rx(false),
    m_rxSkippedSamples(0)
{
    m_params.setParam("orig_test_name",params.c_str());
    m_params.assign(__plugin.name() + "/" + params.c_str());
    debugName(m_params);
    debugChain(&__plugin);
}

bool RadioTest::command(const String& cmd, const NamedList& params)
{
    Debug(this,DebugNote,"Unknown command '%s' [%p]",cmd.c_str(),this);
    return false;
}

bool RadioTest::start(const NamedList& params, const NamedList& radioParams)
{
    s_test = new RadioTest(params,radioParams);
    if (s_test->startup())
	return true;
    delete s_test;
    Debug(&__plugin,DebugNote,"Failed to start test thread");
    return false;
}

void RadioTest::run()
{
    readStop();
    m_started = true;
    notify("start");
    m_init.clearParams();
    bool ok = false;
    unsigned int repeat = m_params.getIntValue("repeat",1,1);
    Debug(this,DebugInfo,"Initializing repeat=%u [%p]",repeat,this);
    String error;
    while (true) {
#define TEST_FAIL_BREAK(str,warn) {\
	error = str; \
	if (warn) \
	    Debug(this,DebugNote,"%s [%p]",error.c_str(),this); \
	ok = false; \
	break; \
    }
	// Init test data
	if (!m_params.getBoolValue("init_only")) {
	    m_tx.enabled = true;
	    m_txYield = m_params.getBoolValue("tx_yield",true);
	    if (!setTxData())
		TEST_FAIL_BREAK("Failed to set TX data",false);
	    m_sendBufCount = m_params.getIntValue("send_buffers",0,0);
	    if (m_sendBufCount)
		m_init.addParam("send_buffers",String(m_sendBufCount));
	    m_rx.enabled = !m_params.getBoolValue("sendonly");
	    if (m_rx.enabled) {
		unsigned int n = m_params.getIntValue("readsamples",256,1);
		m_bufs.reset(n,0);
		m_crt.assign(0,samplesf2bytes(m_bufs.bufSamples()));
		m_aux = m_crt;
		m_extra = m_crt;
		m_bufs.crt.samples = (float*)m_crt.data(0);
		m_bufs.aux.samples = (float*)m_aux.data(0);
		m_bufs.extra.samples = (float*)m_extra.data(0);
		m_init.addParam("readsamples",String(n));
	    }
	}
	// Create radio
	Message m("radio.create");
	m.setParam("module",__plugin.name());
	m.copyParams(m_radioParams);
	bool radioOk = Engine::dispatch(m);
	NamedPointer* np = YOBJECT(NamedPointer,m.getParam(YSTRING("interface")));
	m_radio = np ? YOBJECT(RadioInterface,np) : 0;
	if (!m_radio) {
	    error = m[YSTRING("error")];
	    error.printf("Failed to create radio interface: %s",
		error.safe(radioOk ? "Missing interface" : "Message not handled"));
	    TEST_FAIL_BREAK(error,true);
	}
	np->takeData();
	
	NamedList files("");
	for (ObjList* o = m_params.paramList()->skipNull(); o; o = o->skipNext()) {
	    NamedString* ns = static_cast<NamedString*>(o->get());
	    if (!ns->name().startsWith("file:"))
		continue;
	    String file = *ns;
	    NamedList tmp("");
	    tmp.addParam("now",String(Time::secNow()));
	    tmp.replaceParams(file);
	    if (file && execute("devparam:" + ns->name().substr(5),file,true,0))
		files.addParam(ns->name(),file);
	}
	m_params.clearParam("file",':');
	m_params.copyParams(files);
	
	if (!execute(m_params,"init:"))
	    TEST_FAIL_BREAK("Failed to execute initial command(s)",false);
	unsigned int status = m_radio->initialize(m_radioParams);
	if (status) {
	    if (RadioInterface::Pending == status) {
		unsigned int wait = m_params.getIntValue("wait_pending_init",0,0);
		if (wait)
		    status = m_radio->pollPending(RadioInterface::PendingInitialize,wait);
		else {
		    while (!Thread::check(false)) {
			status = m_radio->pollPending(RadioInterface::PendingInitialize);
			if (!status || RadioInterface::Pending != status)
			    break;
			Thread::idle();
		    }
		    if (Thread::check(false))
			status = RadioInterface::Cancelled;
		}
	    }
	    if (status) {
		if (status != RadioInterface::Cancelled)
		    error.printf("Failed to initialize radio interface: %u %s",
			status,RadioInterface::errorName(status));
		else if (Thread::check(false)) {
		    TEST_FAIL_BREAK("cancelled",false);
		}
		else
		    error = "device closed while initializing";
		TEST_FAIL_BREAK(error,true);
	    }
	}
	if (!execute(m_params,"cmd:"))
	    TEST_FAIL_BREAK("Failed to execute post init command(s)",false);
	if (!wait("wait_after_init"))
	    TEST_FAIL_BREAK("cancelled",false);
	ok = true;
	if (!(m_tx.enabled || m_rx.enabled))
	    break;
	if (m_rx.enabled) {
	    updateTs(false);
	    m_recv = RadioTestRecv::start(this,
		Thread::priority(m_params.getValue("recv_priority")));
	    if (!m_recv)
		TEST_FAIL_BREAK("Failed to start read data thread",true);
	}
	String s;
	m_init.dump(s,"\r\n");
	Debug(this,DebugInfo,"Starting [%p]%s",this,encloseDashes(s,true));
	// Run
	while (!Thread::check(false)) {
	    if (!write())
		TEST_FAIL_BREAK("Send data failed",false);
	    if (m_rx.enabled && !m_recv && !Thread::check(false))
		TEST_FAIL_BREAK("Read data thread abnormally terminated",true);
	}
	if (Thread::check(false))
	    error = "stopped";
	readStop();
#undef TEST_FAIL_BREAK
	break;
    }
    if (ok && (repeat > 1) && !Thread::check(false))
	m_repeat = --repeat;
    terminated(error);
}

void RadioTest::terminated(const char* error)
{
    readStop();
    s_testMutex.lock();
    if (s_test == this)
	s_test = 0;
    s_testMutex.unlock();
    TelEngine::destruct(m_radio);
    if (!m_started)
	return;
    m_started = false;
    RadioTestIO* txrx[2] = {&m_tx,&m_rx};
    uint64_t now = Time::now();
    NamedList report("");
    for (uint8_t i = 0; i < 2; i++) {
	RadioTestIO& io = *(txrx[i]);
	if (!io.enabled)
	    continue;
	String prefix = io.tx ? "tx_" : "rx_";
	report.addParam(prefix + "transferred",String(io.transferred));
	if (io.transferred) {
	    unsigned int sec = (unsigned int)((now - io.startTime) / 1000000);
	    if (sec)
		report.addParam(prefix + "samplerate",String(io.transferred / sec));
	}
	report.addParam(prefix + "ts",String(io.ts));
    }
    if (m_rxSkippedSamples)
	report.addParam("rx_skipped_samples",String(m_rxSkippedSamples));
    String s;
    report.dump(s,"\r\n");
    Debug(this,DebugInfo,"Terminated [%p]%s",this,encloseDashes(s,true));
    notify("stop",error,&report);
    if (!m_repeat)
	return;
    Debug(this,DebugNote,"Restarting repeat=%u [%p]",m_repeat,this);
    Message* m = new Message("chan.control");
    m->addParam("module",__plugin.name());
    m->addParam("component",__plugin.name());
    m->addParam("operation","restart");
    m->addParam("name",m_params.getValue("orig_test_name"));
    m->addParam("repeat",String(m_repeat));
    m->copySubParams(m_params,"file:",false,true);
    Engine::enqueue(m);
}

bool RadioTest::setTxData()
{
    const String& pattern = m_params["txdata"];
    if (!pattern) {
	Debug(this,DebugConf,"Missing tx data pattern [%p]",this);
	return false;
    }
    m_newTxData = true;
    m_phase = 0;
    m_pulse = 0;
    String tp;
    if (pattern == "two-circles") {
	m_sendBufData.resetStorage(m_params.getIntValue("txdata_length",819,50));
	m_init.addParam("txpattern",pattern);
    }
    else if (pattern == "pulse") {
	unsigned int samples = m_params.getIntValue("txdata_length",10000,50);
	m_sendBufData.resetStorage(samples);
	unsigned int defVal = (samples > 2) ? (samples - 2) : 2;
	m_pulse = m_params.getIntValue("pulse",defVal,2,10000000);
	String pattern = m_params.getValue("pulse_pattern","1,1,-1,-1");
	String e;
	bool ok = parseVector(e,pattern,m_pulseData);
	if (!ok || m_pulseData.length() < 2 || m_pulseData.length() > (m_pulse / 3)) {
	    bool sh = (m_pulseData.length() < 2);
	    Debug(this,DebugConf,"Invalid pulse_pattern '%s': %s [%p]",
		pattern.c_str(),e.safe(sh ? "too short" : "too long"),this);
	    return false;
	}
	m_init.addParam("txpattern",pattern);
	m_init.addParam("pulse",String(m_pulse));
	String s;
	unsigned int h = m_pulseData.length() > 10 ? 10 : m_pulseData.length();
	m_pulseData.head(h).dump(s,Math::dumpComplex," ","%g,%g");
	m_init.addParam("pulse_pattern",pattern);
    }
    else {
	m_newTxData = false;
	String e;
	if (!buildVector(e,pattern,m_sendBufData)) {
	    Debug(this,DebugConf,"Invalid tx data pattern '%s': %s [%p]",
		pattern.c_str(),e.safe("unknown"),this);
	    return false;
	}
	unsigned int len = m_sendBufData.length();
	int n = m_params.getIntValue("txdata_repeat");
	if (n > 0) {
	    ComplexVector tmp(m_sendBufData);
	    m_sendBufData.resetStorage(n * len);
	    for (unsigned int i = 0; i < m_sendBufData.length(); i += len)
		m_sendBufData.slice(i,len).copy(tmp,len);
	}
	m_init.addParam("txpattern",pattern.substr(0,50));
	String s;
	m_sendBufData.head(len > 20 ? 20 : len).dump(s,Math::dumpComplex,",","%g,%g");
	if (!s.startsWith(pattern))
	    m_init.addParam("txdata",s);
    }
    m_init.addParam("send_samples",String(m_sendBufData.length()));
    return true;
}

void RadioTest::regenerateTxData()
{
    // Fs / 4 data
    static const float s_cs4[4] = {1,0,-1,0};
    // Fs / 8 data
    static const float s_r2 = M_SQRT1_2;
    static const float s_cs8[8] = {1,s_r2,0,-s_r2,-1,-s_r2,0,s_r2};

    Complex* last = 0;
    Complex* c = m_sendBufData.data(0,m_sendBufData.length(),last);
    if (m_pulse) {
	m_sendBufData.bzero();
	for (; c != last; ++c, ++m_phase) {
	    unsigned int idx = m_phase % m_pulse;
	    if (idx < m_pulseData.length())
		*c = m_pulseData[idx];
	}
    }
    else
	for (; c != last; ++c, ++m_phase)
	    c->set(0.5 * (s_cs4[m_phase % 4] + s_cs8[m_phase % 8]),
		-0.5 * (s_cs4[(m_phase + 1) % 4] + s_cs8[(m_phase + 2) % 8]));
}

bool RadioTest::execute(const String& cmd, const String& param, bool fatal,
    const NamedList* params)
{
    XDebug(this,DebugAll,"execute(%s,%s) [%p]",cmd.c_str(),param.c_str(),this);
    unsigned int c = RadioInterface::Failure;
    if (cmd == YSTRING("samplerate"))
	c = m_radio->setSampleRate(param.toInteger());
    else if (cmd == YSTRING("filter"))
	c = m_radio->setFilter(param.toInteger());
    else if (cmd == YSTRING("txfrequency"))
	c = m_radio->setTxFreq(param.toInteger());
    else if (cmd == YSTRING("rxfrequency"))
	c = m_radio->setRxFreq(param.toInteger());
    else if (cmd == YSTRING("loopback"))
	c = m_radio->setLoopback(param);
    else if (cmd == YSTRING("calibrate"))
	c = m_radio->calibrate();
    else if (cmd.startsWith("devparam:")) {
	NamedList tmp("");
	if (params)
	    tmp.copySubParams(*params,cmd + "_");
	tmp.setParam("cmd:" + cmd,param);
	c = m_radio->setParams(tmp);
    }
    else {
	Debug(this,DebugNote,"Unhandled command '%s' [%p]",cmd.c_str(),this);
	return true;
    }
    if (c == 0 || !fatal)
	return true;
    Debug(this,DebugNote,"'%s' failed with %u '%s' [%p]",cmd.c_str(),c,
	RadioInterface::errorName(c),this);
    return false;
}

bool RadioTest::execute(const NamedList& cmds, const char* prefix)
{
    for (const ObjList* o = cmds.paramList()->skipNull(); o; o = o->skipNext()) {
	const NamedString* ns = static_cast<const NamedString*>(o->get());
	String s = ns->name();
	if (s.startSkip(prefix,false) &&
	    !execute(s,*ns,cmds.getBoolValue(s + "_fatal",true),&cmds))
	    return false;
    }
    return true;
}

bool RadioTest::write()
{
    if (!m_tx.startTime)
	m_tx.startTime = Time::now();
    if (m_newTxData)
	regenerateTxData();
    else if (m_txYield)
	Thread::yield(false);
    if (!m_tx.ts)
	updateTs(true);
    unsigned int code = m_radio->send(m_tx.ts,(float*)m_sendBufData.data(),
	m_sendBufData.length());
    if (!code) {
	m_tx.ts += m_sendBufData.length();
	m_tx.transferred += m_sendBufData.length();
	if (!m_sendBufCount)
	    return true;
	m_sendBufCount--;
	return m_sendBufCount > 0;
    }
    if (code != RadioInterface::Cancelled)
	Debug(this,DebugNote,"Send error: %u '%s' [%p]",
	    code,RadioInterface::errorName(code),this);
    return false;
}

bool RadioTest::read()
{
    if (!m_rx.startTime)
	m_rx.startTime = Time::now();
    if (!m_rx.ts)
	updateTs(false);
    unsigned int skippedBuffs = 0;
    unsigned int code = m_radio->read(m_rx.ts,m_bufs,skippedBuffs);
    if (skippedBuffs) {
	uint64_t skipped = skippedBuffs * m_bufs.bufSamples();
	m_rxSkippedSamples += skipped;
	m_rx.transferred += skipped;
    }
    if (!code) {
	if (m_bufs.full(m_bufs.crt))
	    m_rx.transferred += m_bufs.bufSamples();
	return true;
    }
    if (code != RadioInterface::Cancelled)
	Debug(this,DebugNote,"Recv error: %u '%s' [%p]",
	    code,RadioInterface::errorName(code),this);
    return false;
}

void RadioTest::readTerminated(RadioTestRecv* th)
{
    Lock lck(s_testMutex);
    if (m_recv == th)
	m_recv = 0;
}

void RadioTest::readStop()
{
    if (!m_recv)
	return;
    Lock lck(s_testMutex);
    if (!m_recv)
	return;
    m_recv->cancel();
    lck.drop();
    // Wait for 5 seconds before hard cancelling
    unsigned int n = threadIdleIntervals(5000);
    while (m_recv && n) {
	Thread::idle();
	n--;
    }
    lck.acquire(s_testMutex);
    if (m_recv)
	hardCancelRecv();
}

void RadioTest::hardCancelRecv()
{
    if (!m_recv)
	return;
    Debug(this,DebugWarn,"Hard cancelling read data thread (%p) [%p]",m_recv,this);
    m_recv->cancel(true);
    m_recv = 0;
}

bool RadioTest::wait(const String& param)
{
    unsigned int wait = m_params.getIntValue(param,0,0);
    if (!wait)
	return true;
    Debug(this,DebugInfo,"Waiting '%s' %ums [%p]",param.c_str(),wait,this);
    unsigned int n = threadIdleIntervals(wait);
    for (; n && !Thread::check(false); n--)
	Thread::idle();
    return n == 0;
}

void RadioTest::notify(const char* state, const char* error, const NamedList* params) const
{
    const String& id = m_params[YSTRING("notify")];
    if (!id)
	return;
    Message* m = new Message(m_params.getValue(YSTRING("notifymsg"),"chan.notify"));
    m->addParam("module",__plugin.name());
    m->addParam("test_name",debugName());
    m->addParam("id",id);
    m->addParam("state",state);
    m->addParam("error",error,false);
    if (params)
	m->copyParams(*params);
    Engine::enqueue(m);
}


//
// RadioTestModule
//
RadioTestModule::RadioTestModule()
    : Module("radiotest","misc")
{
    Output("Loaded module Radio Test");
}

RadioTestModule::~RadioTestModule()
{
    Output("Unloading module Radio Test");
    if (s_test)
	Debug(this,DebugWarn,"Exiting while test is running!!!");
}

void RadioTestModule::initialize()
{
    Output("Initializing module Radio Test");
    if (!relayInstalled(Halt)) {
	setup();
	installRelay(Halt,120);
	installRelay(Control);
    }
}

bool RadioTestModule::received(Message& msg, int id)
{
    if (id == Control) {
	if (msg[YSTRING("component")] == name())
	    return onCmdControl(msg);
	return false;
    }
    else if (id == Halt)
	test();
    return Module::received(msg,id);
}

bool RadioTestModule::commandComplete(Message& msg, const String& partLine,
    const String& partWord)
{
    if (partLine == YSTRING("control")) {
	itemComplete(msg.retValue(),name(),partWord);
	return false;
    }
    String tmp = partLine;
    if (tmp.startSkip("control") && tmp == name()) {
	// Complete commands
	itemComplete(msg.retValue(),"start",partWord);
	itemComplete(msg.retValue(),"exec",partWord);
	itemComplete(msg.retValue(),"stop",partWord);
	itemComplete(msg.retValue(),"radiodatafile",partWord);
	itemComplete(msg.retValue(),"help",partWord);
	return false;
    }
    return Module::commandComplete(msg,partLine,partWord);
}

bool RadioTestModule::onCmdControl(Message& msg)
{
    static const char* s_help =
	"\r\ncontrol radiotest {start [name=conf_sect_name]|stop|exec}"
	"\r\n  Test commands"
	"\r\ncontrol radiotest radiodatafile [sect=conf_sect_name]"
	"\r\n  Read radio data file. Process it according to given section parameters.";

    const String& cmd = msg[YSTRING("operation")];
    if (cmd == YSTRING("help")) {
	msg.retValue() << s_help;
	return true;
    }
    if (cmd == YSTRING("radiodatafile")) {
	processRadioDataFile(msg);
	return true;
    }
    return test(cmd,msg);
}

// control module_name test oper={start|stop|.....} params...
bool RadioTestModule::test(const String& cmd, const NamedList& list)
{
    static bool s_exec = false;

    Lock lck(s_testMutex);
    while (s_exec) {
	lck.drop();
	Thread::idle();
	if (Thread::check(false))
	    return false;
	lck.acquire(s_testMutex);
    }
    bool bRet = true;
    bool start = (cmd == YSTRING("start"));
    bool restart = !start && (cmd == YSTRING("restart"));
    if (start || restart || !cmd || cmd == YSTRING("stop")) {
	// Stop the test
	while (s_test) {
	    s_test->cancel();
	    if (s_test->m_recv)
		s_test->m_recv->cancel();
	    lck.drop();
	    // Wait for 5 seconds before hard cancelling
	    unsigned int n = threadIdleIntervals(5000);
	    while (s_test && n) {
		Thread::idle();
		n--;
	    }
	    lck.acquire(s_testMutex);
	    if (!s_test)
		break;
	    s_test->hardCancelRecv();
	    Debug(this,DebugWarn,"Hard cancelling test thread (%p)",s_test);
	    s_test->cancel(true);
	    s_test = 0;
	}
	while (start || restart) {
	    bRet = false;
	    const String& over = start ? list[YSTRING("override")] : String::empty();
	    const char* n = 0;
	    if (start)
		n = list.getValue("test_name","test");
	    NamedList params(list.getValue(YSTRING("name"),n));
	    if (!params) {
		Debug(this,DebugNote,"Failed to start test: missing name");
		break;
	    }
	    Configuration cfg(Engine::configFile(name()),!over);
	    NamedList* sect = cfg.getSection(params);
	    if (sect) {
		const char* inc = sect->getValue(YSTRING("include"));
		if (inc)
		    params.copyParams(*cfg.createSection(inc));
		params.copyParams(*sect);
	    }
	    else if (!over) {
		Debug(this,DebugNote,"Failed to start test '%s': missing config section",
		    params.c_str());
		break;
	    }
	    const char* radioSect = params.getValue("radio_section","radio");
	    NamedList radioParams(*cfg.createSection(radioSect));
	    if (over) {
		// Boolean true: copy all parameters
		// Otherwise: it indicates a prefix
		if (over.toBoolean())
		    params.copyParams(list);
		else
		    params.copySubParams(list,over,true,true);
		radioParams.copySubParams(list,"radio_",true,true);
	    }
	    else if (restart) {
		unsigned int repeat = list.getIntValue("repeat",0,0);
		if (!repeat)
		    break;
		params.setParam("repeat",String(repeat));
		params.clearParam("file",':');
		params.copySubParams(list,"file:",false,true);
	    }
	    params.setParam(YSTRING("first"),String::boolText(start));
	    bRet = RadioTest::start(params,radioParams);
	    break;
	}
    }
    else if (s_test)
	bRet = s_test->command(cmd,list);
    else
	Debug(this,DebugInfo,"Test is not running");
    s_exec = false;
    return bRet;
}

void RadioTestModule::processRadioDataFile(NamedList& params)
{
    Configuration cfg(Engine::configFile(name()));
    const char* s = params.getValue("sect","radiodatafile");
    NamedList* p = cfg.getSection(s);
    if (!p) {
	Debug(this,DebugNote,
	    "Can't handle radio data file process: no section '%s' in config",s);
	return;
    }
    const char* file = p->getValue("input");
    if (!file) {
	Debug(this,DebugNote,"Radio data file process sect='%s': missing file",s);
	return;
    }
    RadioDataFile d("RadioTest");
    if (!d.open(file,0,this))
	return;
    const RadioDataDesc& desc = d.desc();
    String error;
#define RADIO_FILE_ERROR(what,value) { \
    error << what << " " << value; \
    break; \
}
    while (true) {
	if (desc.m_signature[2])
	    RADIO_FILE_ERROR("unhandled version",desc.m_signature[2]);
	if (desc.m_sampleLen != 2)
	    RADIO_FILE_ERROR("unhandled sample length",desc.m_sampleLen);
	if (desc.m_ports != 1)
	    RADIO_FILE_ERROR("unhandled ports",desc.m_ports);
	String fmt;
	unsigned int sz = 0;
	switch (desc.m_elementType) {
	    case RadioDataDesc::Float:
		fmt = p->getValue(YSTRING("fmt-float"),"%+g%+gj");
		sz = sizeof(float);
		break;
	    case RadioDataDesc::Int16:
		fmt = p->getValue(YSTRING("fmt-int"),"%+d%+dj");
		sz = sizeof(int16_t);
		break;
	    default:
		RADIO_FILE_ERROR("unhandled element type",desc.m_elementType);
	}
	if (error)
	    break;
	File fOut;
	const String& output = (*p)[YSTRING("output")];
	if (output && !fOut.openPath(output,true,false,true,false,false,true)) {
	    String tmp;
	    Thread::errorString(tmp,fOut.error());
	    error.printf("Failed to open output file '%s' - %d %s",
		output.c_str(),fOut.error(),tmp.c_str());
	    break;
	}
	const String* sepParam = p->getParam(YSTRING("separator"));
	const char* sep = sepParam ? sepParam->c_str() : " ";
	bool dumpData = fOut.valid() ? true : p->getBoolValue(YSTRING("dumpdata"),true);
	unsigned int dumpStart = p->getIntValue(YSTRING("recstart"),1,1);
	unsigned int dumpCount = p->getIntValue(YSTRING("reccount"),0,0);
	unsigned int dumpMax = p->getIntValue(YSTRING("recsamples"),0,0);
	const String& recFmt = (*p)[YSTRING("recformat")];
	Debug(this,DebugAll,"Processing radio data file '%s'",file);
	NamedList special("");
	special.addParam("newline","\r\n");
	special.addParam("tab","\t");
	special.replaceParams(fmt);
	String fmt4 = fmt + sep + fmt + sep + fmt + sep + fmt;
	uint64_t ts = 0;
	DataBlock buf;
	unsigned int n = 0;
	uint64_t oldTs = 0;
	bool first = true;
	unsigned int sampleBytes = sz * 2;
	while (!Thread::check(false) && d.read(ts,buf,this) && buf.length()) {
	    n++;
	    if ((buf.length() % sampleBytes) != 0) {
		error.printf("record=%u len=%u - length is not a multiple of samples",
		    n,buf.length());
		break;
	    }
	    if (n < dumpStart)
		continue;
	    String str;
	    if (dumpData) {
		if (!d.sameEndian() && !d.fixEndian(buf,sz))
		    RADIO_FILE_ERROR("unhandled endiannes for element type",desc.m_elementType);
		switch (desc.m_elementType) {
		    case RadioDataDesc::Float:
			dumpSamplesFloat(str,buf,fmt4,fmt,sep,dumpMax);
			break;
		    case RadioDataDesc::Int16:
			dumpSamplesInt16(str,buf,fmt4,fmt,sep,dumpMax);
			break;
		    default:
			RADIO_FILE_ERROR("unhandled element type",desc.m_elementType);
		}
		if (error)
		    break;
	    }
	    int64_t delta = 0;
	    if (first)
		first = false;
	    else
		delta = ts - oldTs;
	    oldTs = ts;
	    unsigned int samples = buf.length() / sampleBytes;
	    if (fOut.valid()) {
		if (str) {
		    if (recFmt) {
			NamedList nl("");
			nl.addParam("timestamp",String(ts));
			nl.addParam("data",str);
			nl.addParam("ts-delta",String(delta));
			nl.addParam("samples",String(samples));
			nl.addParam("newline","\r\n");
			nl.addParam("separator",sep);
			str = recFmt;
			nl.replaceParams(str);
		    }
		    else
			str += sep;
		    int wr = str ? fOut.writeData(str.c_str(),str.length()) : str.length();
		    if (wr != (int)str.length()) {
			String tmp;
			Thread::errorString(tmp,fOut.error());
			error.printf("Failed to write (%d/%u) output file '%s' - %d %s",
			    wr,str.length(),output.c_str(),fOut.error(),tmp.c_str());
			break;
		    }
		}
	    }
	    else
		Output("%u: TS=" FMT64U " bytes=%u samples=%u delta=" FMT64 "%s",
		    n,ts,buf.length(),samples,delta,encloseDashes(str,true));
	    if (dumpCount) {
		dumpCount--;
		if (!dumpCount)
		    break;
	    }
	}
	break;
    }
#undef RADIO_FILE_ERROR
    if (error)
	Debug(this,DebugNote,"Processing radio data file '%s': %s",file,error.c_str());
}

}; // anonymous namespace

/* vi: set ts=8 sw=4 sts=4 noet enc=utf-8: */
