/*
    PIP - Platform Independent Primitives
    GPIO
    Andrey Bychkov work.a.b@yandex.ru, Ivan Pelipenko peri4ko@yandex.ru

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU Lesser 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 Lesser General Public License for more details.

    You should have received a copy of the GNU Lesser General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

#include "pigpio.h"

#include "pitime.h"
#ifdef LINUX
#  define GPIO_SYS_CLASS
#endif
#ifdef GPIO_SYS_CLASS
#  include <cstdlib>
#  include <fcntl.h>
#  include <unistd.h>
#endif
#include "piliterals.h"


//! \class PIGPIO pigpio.h
//! \~english \section PIGPIO_sec0 Synopsis
//! \~russian \section PIGPIO_sec0 Краткий обзор
//! \~english
//! This class provide initialize, get/set and watch functions for GPIO.
//!
//! This class should be used with \a PIGPIO::instance() singleton.
//!
//! Currently supported only "/sys/class/gpio" mechanism on Linux.
//!
//!
//! \~russian
//! Этот класс предоставляет инициализацию, установку, чтение и наблюдение
//! за GPIO.
//!
//! Этот класс используется только через синглтон \a PIGPIO::instance().
//!
//! Сейчас поддерживается только "/sys/class/gpio" для Linux.
//!
//!
//! \~\section PIGPIO_sec1 API
//! \~english
//! There are several function to directly read or write pin states.
//!
//! Also you can start %PIGPIO as thread to watch pin states and receive
//! \a pinChanged() event.
//!
//!
//! \~russian
//! Имеется несколько методов для прямой записи и чтения состояний пинов.
//!
//! Также можно запустить %PIGPIO как поток для наблюдения за пинами и
//! принимать события \a pinChanged().
//!


PIGPIO::PIGPIO(): PIThread() {}


PIGPIO::~PIGPIO() {
	stop();
	waitForFinish(100_ms);
	PIMutexLocker ml(mutex);
#ifdef GPIO_SYS_CLASS
	PIVector<int> ids = gpio_.keys();
	for (int i = 0; i < ids.size_s(); i++) {
		GPIOData & g(gpio_[ids[i]]);
		if (g.fd != -1) {
			::close(g.fd);
			g.fd = -1;
		}
	}
	gpio_.clear();
#endif
}


PIGPIO * PIGPIO::instance() {
	static PIGPIO ret;
	return &ret;
}


PIString PIGPIO::GPIOName(int gpio_num) {
	return PIStringAscii("gpio") + PIString::fromNumber(gpio_num);
}


void PIGPIO::exportGPIO(int gpio_num) {
#ifdef GPIO_SYS_CLASS
	PIString valfile = "/sys/class/gpio/" + GPIOName(gpio_num) + "/value";
	int fd           = ::open(valfile.dataAscii(), O_RDONLY);
	if (fd != -1) {
		::close(fd);
		return;
	}
	int ret = 0;
	ret     = system(PIString("echo " + PIString::fromNumber(gpio_num) + " >> /sys/class/gpio/export").dataAscii());
	if (ret == 0) {
		PITimeMeasurer tm;
		while (tm.elapsed_s() < 1.) {
			fd = ::open(valfile.dataAscii(), O_RDWR);
			if (fd != -1) {
				::close(fd);
				return;
			}
			piMSleep(1);
		}
	}
#endif
}


void PIGPIO::openGPIO(GPIOData & g) {
#ifdef GPIO_SYS_CLASS
	if (g.fd != -1) {
		::close(g.fd);
		g.fd = -1;
	}
	PIString fp = "/sys/class/gpio/" + g.name + "/value";
	g.fd        = ::open(fp.dataAscii(), O_RDWR);
	// piCoutObj << "initGPIO" << g.num << ":"  << fp << g.fd << errorString();
#endif
}


bool PIGPIO::getPinState(int gpio_num) {
#ifdef GPIO_SYS_CLASS
	GPIOData & g(gpio_[gpio_num]);
	char r  = 0;
	int ret = 0;
	if (g.fd != -1) {
		::lseek(g.fd, 0, SEEK_SET);
		ret = ::read(g.fd, &r, sizeof(r));
		if (ret > 0) {
			if (r == '1') return true;
			if (r == '0') return false;
		}
	}
	// piCoutObj << "pinState" << gpio_num << ":" << ret << (int)r << errorString();
#endif
	return false;
}


void PIGPIO::begin() {
	PIMutexLocker ml(mutex);
	if (watch_state.isEmpty()) return;
	PIVector<int> ids = watch_state.keys();
	for (int i = 0; i < ids.size_s(); i++) {
		GPIOData & g(gpio_[ids[i]]);
		if (g.num != -1 && !g.name.isEmpty()) {
			openGPIO(g);
			watch_state[ids[i]] = getPinState(ids[i]);
		}
	}
}


void PIGPIO::run() {
	mutex.lock();
	if (watch_state.isEmpty()) {
		mutex.unlock();
		return;
	}
	PIVector<PIPair<int, bool>> changed;
	auto it = watch_state.makeIterator();
	while (it.next()) {
		GPIOData & g(gpio_[it.key()]);
		if (g.num == -1 || g.name.isEmpty()) continue;
		bool v = getPinState(g.num);
		// piCoutObj << "*** pin state ***" << ids[i] << "=" << v;
		if (watch_state[g.num] != v) {
			watch_state[g.num] = v;
			changed.push_back({g.num, v});
		}
	}
	mutex.unlock();
	for (const auto & i: changed)
		pinChanged(i.first, i.second);
}


void PIGPIO::end() {
	PIMutexLocker ml(mutex);
	if (watch_state.isEmpty()) return;
	PIVector<int> ids = watch_state.keys();
	for (int i = 0; i < ids.size_s(); i++) {
		GPIOData & g(gpio_[ids[i]]);
		if (g.fd != -1) {
#ifdef GPIO_SYS_CLASS
			::close(g.fd);
#endif
			g.fd = -1;
		}
	}
}


void PIGPIO::initPin(int gpio_num, Direction dir) {
#ifdef GPIO_SYS_CLASS
	PIMutexLocker ml(mutex);
	GPIOData & g(gpio_[gpio_num]);
	if (g.num == -1) {
		g.num  = gpio_num;
		g.name = GPIOName(gpio_num);
		exportGPIO(gpio_num);
	}
	g.dir   = dir;
	int ret = 0;
	NO_UNUSED(ret);
	switch (dir) {
	case In: ret = system(("echo in >> /sys/class/gpio/" + g.name + "/direction").dataAscii()); break;
	case Out: ret = system(("echo out >> /sys/class/gpio/" + g.name + "/direction").dataAscii()); break;
	default: break;
	}
	openGPIO(g);
#endif
}


void PIGPIO::pinSet(int gpio_num, bool value) {
#ifdef GPIO_SYS_CLASS
	PIMutexLocker ml(mutex);
	GPIOData & g(gpio_[gpio_num]);
	int ret = 0;
	NO_UNUSED(ret);
	if (g.fd != -1) {
		if (value)
			ret = ::write(g.fd, "1", 1);
		else
			ret = ::write(g.fd, "0", 1);
	}
	// piCoutObj << "pinSet" << gpio_num << ":" << ret << errorString();
#endif
}


bool PIGPIO::pinState(int gpio_num) {
	PIMutexLocker ml(mutex);
	return getPinState(gpio_num);
}


//! \~\details
//! \~english
//! This function doesn`t affect thread state!
//! Pins watching starts only with \a PIThread::start() function
//!
//! \~russian
//! Этот метод не меняет состояние потока наблюдения!
//! Наблюдение за пинами начинается методом \a PIThread::start()
void PIGPIO::pinBeginWatch(int gpio_num) {
	PIMutexLocker ml(mutex);
	GPIOData & g(gpio_[gpio_num]);
	if (g.fd != -1) {
#ifdef GPIO_SYS_CLASS
		::close(g.fd);
#endif
		g.fd = -1;
	}
	watch_state.insert(gpio_num, false);
}


//! \~\details
//! \~english
//! This function doesn`t affect thread state!
//! Pins watching starts only with \a PIThread::start() function
//!
//! \~russian
//! Этот метод не меняет состояние потока наблюдения!
//! Наблюдение за пинами начинается методом \a PIThread::start()
void PIGPIO::pinEndWatch(int gpio_num) {
	PIMutexLocker ml(mutex);
	watch_state.remove(gpio_num);
}


//! \~\details
//! \~english
//! This function doesn`t affect thread state!
//! Pins watching starts only with \a PIThread::start() function
//!
//! \~russian
//! Этот метод не меняет состояние потока наблюдения!
//! Наблюдение за пинами начинается методом \a PIThread::start()
void PIGPIO::clearWatch() {
	PIMutexLocker ml(mutex);
	watch_state.clear();
}


#ifdef __GNUC__
// #  pragma GCC diagnostic pop
#endif
