/*
    PIP - Platform Independent Primitives
    Abstract input/output device
    Copyright (C) 2016  Ivan Pelipenko peri4ko@gmail.com

    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 3 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, see <http://www.gnu.org/licenses/>.
*/

#include "piiodevice.h"
#include "piconfig.h"
#include "piconnection.h"


/*! \class PIIODevice
 * \brief Base class for input/output classes
 * 
 * \section PIIODevice_sec0 Synopsis
 * This class provide open/close logic, threaded read/write and virtual input/output
 * functions \a read() and \a write(). You should implement pure virtual
 * function \a openDevice() in your subclass.
 * 
 * \section PIIODevice_sec1 Open and close
 * PIIODevice have boolean variable indicated open status. Returns of functions
 * \a openDevice() and \a closeDevice() change this variable.
 * 
 * \section PIIODevice_sec2 Threaded read
 * PIIODevice based on PIThread, so it`s overload \a run() to exec \a read()
 * in background thread. If read is successful virtual function \a threadedRead()
 * is executed. Default implementation of this function execute external static
 * function set by \a setThreadedReadSlot() with data set by \a setThreadedReadData().
 * Extrenal static function should have format \n
 * bool func_name(void * Threaded_read_data, uchar * readed_data, int readed_size)\n
 * Threaded read starts with function \a startThreadedRead().
 * 
 * \section PIIODevice_sec3 Threaded write
 * PIIODevice aggregate another PIThread to perform a threaded write by function
 * \a writeThreaded(). This function add task to internal queue and return
 * queue entry ID. You should start write thread by function \a startThreadedWrite.
 * On successful write event \a threadedWriteEvent is raised with two arguments - 
 * task ID and written bytes count.
 * 
 * \section PIIODevice_sec4 Internal buffer
 * PIIODevice have internal buffer for threaded read, and \a threadedRead() function 
 * receive pointer to this buffer in first argument. You can adjust size of this buffer
 * by function \a setThreadedReadBufferSize() \n
 * Default size of this buffer is 4096 bytes.
 * 
 * \section PIIODevice_sec5 Reopen
 * When threaded read is begin its call \a open() if device is closed. While threaded
 * read running PIIODevice check if device opened every read and if not call \a open()
 * every reopen timeout if reopen enabled. Reopen timeout is set by \a setReopenTimeout(),
 * reopen enable is set by \a setReopenEnabled().
 * 
 * \section PIIODevice_sec6 Configuration
 * This is virtual function \a configureDevice() which executes when \a configure()
 * executes. This function takes two arguments: "e_main" and "e_parent" as void*. There
 * are pointers to PIConfig::Entry entries of section "section" and their parent. If
 * there is no parent "e_parent" = 0. Function \a configure() set three parameters of
 * device: "reopenEnabled", "reopenTimeout" and "threadedReadBufferSize", then execute
 * function \a configureDevice().
 * \n Each ancestor of %PIIODevice reimlements \a configureDevice() function to be able
 * to be confured from configuration file. This parameters described at section
 * "Configurable parameters" in the class reference. \n Usage example:
 * \snippet piiodevice.cpp configure
 * Implementation example:
 * \snippet piiodevice.cpp configureDevice
 * 
 * \section PIIODevice_sec7 Creating devices by unambiguous string
 * There are some virtual functions to describe child class without its declaration.
 * \n \a fullPathPrefix() should returns unique prefix of device
 * \n \a constructFullPath() should returns full unambiguous string, contains prefix and all device parameters
 * \n \a configureFromFullPath() provide configuring device from full unambiguous string without prefix and "://"
 * \n Macro PIIODEVICE should be used instead of PIOBJECT
 * \n Macro REGISTER_DEVICE should be used after definition of class, i.e. at the last line of *.cpp file
 * \n \n If custom I/O device corresponds there rules, it can be returned by function \a createFromFullPath().
 * \n Each PIP I/O device has custom unambiguous string description:
 * * PIFile: "file://<path>"
 * * PIBinaryLog: "binlog://<logDir>[:<filePrefix>][:<defaultID>]"
 * * PISerial: "ser://<device>:<speed(50|...|115200)>[:<dataBitsCount(6|7|8)>][:<parity(N|E|O)>][:<stopBits(1|2)>]"
 * * PIEthernet: UDP "eth://UDP:<readIP>:<readPort>:<sendIP>:<sendPort>[:<multicast(mcast:<ip>)>]"
 * * PIEthernet: TCP "eth://TCP:<IP>:<Port>"
 * * PIUSB: "usb://<vid>:<pid>[:<deviceNumber>][:<readEndpointNumber>][:<writeEndpointNumber>]"
 * \n \n Examples:
 * * PIFile: "file://../text.txt"
 * * PIBinaryLog: "binlog://../logs/:mylog_:1"
 * * PISerial: "ser:///dev/ttyUSB0:9600:8:N:1", equivalent "ser:///dev/ttyUSB0:9600"
 * * PIEthernet: "eth://TCP:127.0.0.1:16666", "eth://UDP:192.168.0.5:16666:192.168.0.6:16667:mcast:234.0.2.1:mcast:234.0.2.2"
 * * PIUSB: "usb://0bb4:0c86:1:1:2"
 * \n \n
 * So, custom I/O device can be created with next call:
 * \code{cpp}
 * // creatring devices
 * PISerial * ser = (PISerial * )PIIODevice::createFromFullPath("ser://COM1:115200");
 * PIEthernet * eth = (PIEthernet * )PIIODevice::createFromFullPath("eth://UDP:127.0.0.1:4001:127.0.0.1:4002");
 * // examine devices
 * piCout << ser << ser->properties();
 * piCout << eth << eth->properties();
 * \endcode
 * 
 * \section PIIODevice_ex0 Example
 * \snippet piiodevice.cpp 0
 */


PIIODevice::PIIODevice(): PIThread() {
	mode_ = ReadOnly;
	_init();
	setPath(PIString());
}


/*! \brief Constructs a PIIODevice with path and mode
 * \param path path to device
 * \param type mode for open */
PIIODevice::PIIODevice(const PIString & path, PIIODevice::DeviceMode mode): PIThread() {
	mode_ = mode;
	_init();
	setPath(path);
}


PIIODevice::~PIIODevice() {
	stop();
	if (opened_) {
		closeDevice();
		if (!opened_)
			closed();
	}
}


void PIIODevice::_init() {
	opened_ = init_ = thread_started_ = false;
	raise_threaded_read_ = true;
	ret_func_ = 0;
	ret_data_ = 0;
	tri = 0;
	setReopenEnabled(true);
	setReopenTimeout(1000);
	setThreadedReadBufferSize(4096);
	timer.setName("__S__reopen_timer");
	write_thread.setName("__S__write_thread");
	CONNECT2(void, void * , int, &timer, tickEvent, this, check_start);
	CONNECT(void, &write_thread, started, this, write_func);
}


void PIIODevice::check_start(void * data, int delim) {
	//cout << "check " << tread_started_ << endl;
	if (open()) {
		thread_started_ = true;
		timer.stop();
	}
}


void PIIODevice::write_func() {
	while (!write_thread.isStopping()) {
		while (!write_queue.isEmpty()) {
			if (write_thread.isStopping()) return;
			write_thread.lock();
			PIPair<PIByteArray, ullong> item(write_queue.dequeue());
			write_thread.unlock();
			int ret = write(item.first);
			threadedWriteEvent(item.second, ret);
		}
		msleep(1);
	}
}


void PIIODevice::terminate() {
	timer.stop();
	thread_started_ = false;
	if (!isInitialized()) return;
	if (isRunning()) {
		stop();
		PIThread::terminate();
	}
}


void PIIODevice::begin() {
	//cout << "   begin\n";
	thread_started_ = false;
	if (!opened_) {
		if (open()) {
			thread_started_ = true;
			//cout << "   open && ok\n";
			return;
		}
	} else {
		thread_started_ = true;
		//cout << "   ok\n";
		return;
	}
	//init();
	if (!timer.isRunning() && isReopenEnabled()) timer.start(reopenTimeout());
}


void PIIODevice::run() {
	if (!isReadable()) {
		//cout << "not readable\n";
		PIThread::stop();
		return;
	}
	if (!thread_started_) {
		msleep(5);
		//cout << "not started\n";
		return;
	}
	readed_ = read(buffer_tr.data(), buffer_tr.size_s());
	if (readed_ <= 0) {
		msleep(10);
		//cout << readed_ << ", " << errno << ", " << errorString() << endl;
		return;
	}
	//piCoutObj << "readed" << readed_;// << ", " << errno << ", " << errorString();
	threadedRead(buffer_tr.data(), readed_);
	if (raise_threaded_read_) threadedReadEvent(buffer_tr.data(), readed_);
}


PIByteArray PIIODevice::readForTime(double timeout_ms) {
	PIByteArray str;
	if (timeout_ms <= 0.) return str;
	int ret;
	uchar * td = new uchar[threadedReadBufferSize()];
	tm.reset();
	while (tm.elapsed_m() < timeout_ms) {
		ret = read(td, threadedReadBufferSize());
		if (ret <= 0) msleep(1);
		else str.append(td, ret);
	}
	delete[] td;
	return str;
}


ullong PIIODevice::writeThreaded(const PIByteArray & data) {
	write_thread.lock();
	write_queue.enqueue(PIPair<PIByteArray, ullong>(data, tri));
	++tri;
	write_thread.unlock();
	return tri - 1;
}


bool PIIODevice::configure(const PIString & config_file, const PIString & section, bool parent_section) {
	PIConfig conf(config_file, PIIODevice::ReadOnly);
	if (!conf.isOpened()) return false;
	bool ex = true;
	PIConfig::Entry em;
	if (section.isEmpty()) em = conf.rootEntry();
	else em = conf.getValue(section, PIString(), &ex);
	if (!ex) return false;
	PIConfig::Entry * ep = 0;
	if (parent_section) ep = em.parent();
	if (ep != 0) {
		setReopenEnabled(ep->getValue("reopenEnabled", isReopenEnabled(), &ex));
		if (!ex) setReopenEnabled(em.getValue("reopenEnabled", isReopenEnabled()));
		setReopenTimeout(ep->getValue("reopenTimeout", reopenTimeout(), &ex));
		if (!ex) setReopenTimeout(em.getValue("reopenTimeout", reopenTimeout()));
		setThreadedReadBufferSize(ep->getValue("threadedReadBufferSize", int(buffer_tr.size_s()), &ex));
		if (!ex) setThreadedReadBufferSize(em.getValue("threadedReadBufferSize", int(buffer_tr.size_s())));
	} else {
		setReopenEnabled(em.getValue("reopenEnabled", isReopenEnabled()));
		setReopenTimeout(em.getValue("reopenTimeout", reopenTimeout()));
		setThreadedReadBufferSize(em.getValue("threadedReadBufferSize", int(buffer_tr.size_s())));
	}
	return configureDevice(&em, ep);
}


PIIODevice * PIIODevice::createFromFullPath(const PIString & full_path) {
	PIString prefix = full_path.left(full_path.find(":"));
	if (prefix.isEmpty()) return 0;
	PIVector<const PIObject * > rd(PICollection::groupElements("__PIIODevices__"));
	piForeachC (PIObject * d, rd) {
		if (prefix == ((const PIIODevice * )d)->fullPathPrefix()) {
			PIIODevice * nd = ((const PIIODevice * )d)->copy();
			if (nd) nd->configureFromFullPath(full_path.mid(prefix.length() + 3));
			return nd;
		}
	}
	return 0;
}


PIString PIIODevice::normalizeFullPath(const PIString & full_path) {
	static PIMutex nfp_mutex;
	static PIMap<PIString, PIString> nfp_cache;
	PIMutexLocker nfp_ml(nfp_mutex);
	PIString ret = nfp_cache.value(full_path);
	if (!ret.isEmpty())
		return ret;
	//piCout << "normalizeFullPath" << full_path;
	PIString fp; PIIODevice::DeviceMode md;
	PIConnection::splitFullPathWithMode(full_path, &fp, &md);
	PIIODevice * d = createFromFullPath(fp);
	//piCout << "normalizeFullPath" << d;
	if (d == 0) return PIString();
	ret = d->constructFullPath();
	if (md == PIIODevice::ReadOnly) ret += " (ro)";
	if (md == PIIODevice::WriteOnly) ret += " (wo)";
	delete d;
	nfp_cache[full_path] = ret;
	return ret;
}
