#include "stdafx.h"
#include "Serial.h"
#include "HandleStream.h"
#include "Url.h"
#include "Core/Exception.h"
#include "Core/Convert.h"
#include "OS/IORequest.h"

#ifdef WINDOWS
#pragma comment (lib, "advapi32.lib")
#endif
#ifdef LINUX
#include <termios.h>
#include <sys/sysmacros.h>
#include <sys/file.h>
#include <signal.h>

// We can disable the old-style device locks by defining DISABLE_UUCP_LOCKS below.
// #define DISABLE_UUCP_LOCKS

#endif

namespace storm {

	SerialOptions::SerialOptions() : baudrate(9600), byteSize(8), parity(parity::none), stopBits(stop::one) {}

	SerialOptions::SerialOptions(Nat baudrate)
		: baudrate(baudrate), byteSize(8), parity(parity::none), stopBits(stop::one) {}

	SerialOptions::SerialOptions(Nat baudrate, Nat byteSize, parity::SerialParity parity, stop::SerialStopBits stopBits)
		: baudrate(baudrate), byteSize(byteSize), parity(parity), stopBits(stopBits) {}

	void SerialOptions::toS(StrBuf *to) const {
		*to << baudrate << S(",") << byteSize << S(",");
		switch (parity) {
		case parity::none:
			*to << S("none");
			break;
		case parity::even:
			*to << S("even");
			break;
		case parity::odd:
			*to << S("odd");
			break;
		}
		*to << S(",");
		switch (stopBits) {
		case stop::one:
			*to << S("1");
			break;
		case stop::two:
			*to << S("2");
			break;
		}
	}

	Nat SerialOptions::hash() const {
		return baudrate ^ byteSize ^ (parity << 8) ^ (stopBits << 16);
	}

	Bool SerialOptions::operator ==(const SerialOptions &o) const {
		return baudrate == o.baudrate
			&& byteSize == o.byteSize
			&& parity == o.parity
			&& stopBits == o.stopBits;
	}


	SerialIStream::SerialIStream(SerialStream *owner)
		: HandleTimeoutIStream(owner->handle, owner->attachedTo), owner(owner) {
		skipClose();
		skipPos();
	}

	SerialIStream::~SerialIStream() {
		// The socket will close the handle.
		handle = os::Handle();
	}

	void SerialIStream::close() {
		handle = os::Handle();
		owner->closeEnd(SerialStream::closeRead);
	}


	SerialOStream::SerialOStream(SerialStream *owner)
		: HandleTimeoutOStream(owner->handle, owner->attachedTo), owner(owner) {
		skipClose();
		skipPos();
	}

	SerialOStream::~SerialOStream() {
		// The socket will close the handle.
		handle = os::Handle();
	}

	void SerialOStream::close() {
		handle = os::Handle();
		owner->closeEnd(SerialStream::closeWrite);
	}


	SerialStream::SerialStream(os::Handle handle, const os::Thread &attachedTo,
							SerialOptions *options, GcArray<char> *lockFile)
		: handle(handle), attachedTo(attachedTo),
		  opts(new (this) SerialOptions(*options)), lockFile(lockFile) {

		applyOptions();

		in = new (this) SerialIStream(this);
		out = new (this) SerialOStream(this);
	}

	SerialStream::~SerialStream() {
		releaseLock();
		storm::close(handle, attachedTo);
	}

	SerialIStream *SerialStream::input() const {
		return in;
	}

	SerialOStream *SerialStream::output() const {
		return out;
	}

	SerialOptions *SerialStream::options() const {
		return new (this) SerialOptions(*opts);
	}

	void SerialStream::options(SerialOptions *options) {
		opts = new (this) SerialOptions(*options);
	}

	void SerialStream::close() {
		in->close();
		out->close();
	}

	void SerialStream::closeEnd(Nat which) {
		Nat old, w;
		do {
			old = atomicRead(closed);
			w = old | which;
		} while (atomicCAS(closed, old, w) != old);

		if (w == (closeRead | closeWrite) && handle) {
			storm::close(handle, attachedTo);
			releaseLock();
		}
	}


	SerialPort::SerialPort(Str *id) : identifier(id) {}

	void SerialPort::toS(StrBuf *to) const {
		*to << S("serial port: ") << identifier;
	}

	Nat SerialPort::hash() const {
		return identifier->hash();
	}

	Bool SerialPort::operator ==(const SerialPort &other) const {
		return *identifier == *other.identifier;
	}


#ifdef WINDOWS

	void SerialStream::applyOptions() {
		DCB state;
		zeroMem(state);
		state.DCBlength = sizeof(DCB);
		state.BaudRate = opts->baudrate;
		state.ByteSize = opts->byteSize;
		state.fBinary = 1;
		state.fParity = (opts->parity != parity::none) ? 1 : 0;
		switch (opts->parity) {
		case parity::none:
			state.Parity = NOPARITY;
			break;
		case parity::even:
			state.Parity = EVENPARITY;
			break;
		case parity::odd:
			state.Parity = ODDPARITY;
			break;
		}
		switch (opts->stopBits) {
		case stop::one:
			state.StopBits = ONESTOPBIT;
			break;
		case stop::two:
			state.StopBits = TWOSTOPBITS;
			break;
		}
		SetCommState(handle.v(), &state);
	}

	bool SerialStream::acquireLock(Str *port, os::Handle handle, GcArray<char> *&lockFile) {
		// No need, Windows is quite strict with Open already.
		(void)port;
		(void)handle;
		(void)lockFile;
		return true;
	}

	void SerialStream::releaseLock() {
		// No need, Windows is quite strict with Open already.
	}

#define USE_THREAD_OPEN 1

#if USE_THREAD_OPEN

	struct OpenParams {
		String path;
		HANDLE output;
		os::Sema sema;
	};

	DWORD WINAPI openComPort(LPVOID params) {
		OpenParams *p = (OpenParams *)params;
		p->output = CreateFile(p->path.c_str(),
							GENERIC_READ | GENERIC_WRITE,
							0,
							NULL,
							OPEN_EXISTING,
							FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED,
							NULL);
		p->sema.up();
		return 0;
	}

#endif

	MAYBE(SerialStream *) SerialPort::open(SerialOptions *options) {
#if USE_THREAD_OPEN
		OpenParams params = {
			String(L"\\\\.\\") + identifier->c_str(),
			NULL,
			os::Sema(0)
		};
		DWORD threadId = 0;
		HANDLE threadHandle = CreateThread(NULL, 4096, &openComPort, &params, 0, &threadId);
		CloseHandle(threadHandle);

		params.sema.down();

		os::Handle handle = params.output;
#else

		String path = String(L"\\\\.\\") + identifier->c_str();
		os::Handle handle = CreateFile(path.c_str(),
									GENERIC_READ | GENERIC_WRITE,
									0,
									NULL,
									OPEN_EXISTING,
									FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED,
									NULL);

#endif

		if (!handle)
			return null;

		// Configure timeouts to behave properly:
		COMMTIMEOUTS timeouts;
		zeroMem(timeouts);
		// This means that "read" works as we would expect (modulo the timeout we already have in HandleIStream):
		timeouts.ReadIntervalTimeout = MAXDWORD;
		timeouts.ReadTotalTimeoutMultiplier = MAXDWORD;
		timeouts.ReadTotalTimeoutConstant = MAXDWORD;
		// Output timeout:
		timeouts.WriteTotalTimeoutMultiplier = 0;
		timeouts.WriteTotalTimeoutConstant = 0;
		SetCommTimeouts(handle.v(), &timeouts);

		os::Thread thread = os::Thread::current();
		thread.attach(handle);

		if (handle) {
			try {
				return new (this) SerialStream(handle, thread, options, null);
			} catch (...) {
				if (handle)
					CloseHandle(handle.v());
				throw;
			}
		} else {
			return null;
		}
	}


	// This seems to be what .NET is doing in its standard library.
	Array<SerialPort *> *serialPorts(EnginePtr e) {
		Array<SerialPort *> *result = new (e.v) Array<SerialPort *>();

		HKEY key = 0;
		LSTATUS ok = RegOpenKeyEx(HKEY_LOCAL_MACHINE, L"HARDWARE\\DEVICEMAP\\SERIALCOMM", 0, KEY_QUERY_VALUE, &key);
		if (ok != ERROR_SUCCESS)
			throw new (e.v) InternalError(S("Failed to enumerate serial ports (failed to access registry)."));

		const DWORD bufferSize = 128;
		wchar nameBuffer[bufferSize];
		byte valueBuffer[bufferSize];

		DWORD index = 0;
		while (true) {
			DWORD nameElements = bufferSize;
			DWORD valueElements = bufferSize;
			DWORD type = 0;
			ok = RegEnumValue(key, index, nameBuffer, &nameElements, 0, &type, valueBuffer, &valueElements);
			if (ok != ERROR_SUCCESS)
				break;

			if (type == REG_SZ) {
				*result << new (e.v) SerialPort(new (e.v) Str((wchar *)valueBuffer));
			}

			index++;
		}

		RegCloseKey(key);

		return result;
	}

#endif

#ifdef LINUX

	void SerialStream::applyOptions() {
		struct termios info;
		if (tcgetattr(handle.v(), &info) != 0)
			return;

		if (opts->parity != parity::none) {
			info.c_cflag &= ~PARENB;
		} else {
			info.c_cflag |= PARENB;
		}
		if (opts->stopBits == stop::one) {
			info.c_cflag &= ~CSTOPB;
		} else {
			info.c_cflag |= CSTOPB;
		}

		info.c_cflag &= ~CSIZE;
		if (opts->byteSize == 5) {
			info.c_cflag |= CS5;
		} else if (opts->byteSize == 6) {
			info.c_cflag |= CS6;
		} else if (opts->byteSize == 7) {
			info.c_cflag |= CS7;
		} else if (opts->byteSize == 8) {
			info.c_cflag |= CS8;
		}

		info.c_cflag &= ~CRTSCTS;
		info.c_cflag |= CREAD | CLOCAL; // ignore potentially extra control lines

		info.c_lflag &= ~ICANON;	// No processing.
		info.c_lflag &= ~ECHO;
		info.c_lflag &= ~ECHOE;
		info.c_lflag &= ~ECHONL;
		info.c_lflag &= ~ISIG;

		info.c_iflag &= ~(IXON | IXOFF | IXANY);

		info.c_oflag &= ~OPOST;
		info.c_oflag &= ~ONLCR;

		// Make it behave like a socket:
		info.c_cc[VMIN] = 1;
		info.c_cc[VTIME] = 0;

		cfsetispeed(&info, opts->baudrate);
		cfsetospeed(&info, opts->baudrate);

		// Set everything:
		if (tcsetattr(handle.v(), TCSANOW, &info) != 0) {
			perror("Failed setting serial port options");
		}
	}

	bool SerialStream::acquireLock(Str *port, os::Handle handle, GcArray<char> *&lockFile) {
		// On Linux/UNIX systems, we need to try to lock the devices manually. There are two
		// different approaches for this. One is to use flock() for advisory locks, and another is
		// to place files in /var/lock. The latter seems to be phased out (both by systemd and by
		// Debian), so we will only use it as a fallback.

		// Start with flock:
		if (::flock(handle.v(), LOCK_EX | LOCK_NB) != 0)
			return false;

#ifndef DISABLE_UUCP_LOCKS
		// Now, try to create a lock file. We use the convention from here:
		// https://refspecs.linuxfoundation.org/FHS_2.3/fhs-2.3.html#VARLOCKLOCKFILES
		StrBuf *lockName = new (port) StrBuf();
		*lockName << S("/var/lock/LCK..");

		Str::Iter pos = port->begin();
		if (port->startsWith(S("/dev/")))
			pos = pos + 5;
		for (; pos != port->end(); ++pos) {
			if (pos.v() != Char('/'))
				*lockName << pos.v();
			else
				*lockName << S("_");
		}

		GcArray<char> *name = toChar(lockName->engine(), lockName->toS()->c_str());

		int lockfd = ::open(name->v, O_RDONLY);
		if (lockfd >= 0) {
			char buffer[16] = { 0 };
			ssize_t r = ::read(lockfd, buffer, sizeof(buffer) - 1);
			::close(lockfd);

			// Make it 0-terminated:
			if (r >= 0)
				buffer[r] = 0;

			long pid = strtol(buffer, NULL, 10);
			if (pid < 0) {
				// Invalid or empty number. Assume someone else was in the process of acquiring the
				// lock.
				return false;
			}

			// Note: kill with signal 0 only checks if we are able to send a signal. We use it to
			// detect if the process exists or not.
			if (::kill(pid_t(pid), 0) < 0 && errno == ESRCH) {
				// The lock refers to a PID that no longer exists. We can remove the lock.
				::unlink(name->v);
			} else {
				// Valid lock, process holding it is alive.
				return false;
			}
		}

		// Acquire the lock:
		char pidbuff[16];
		snprintf(pidbuff, sizeof(pidbuff), "%10d\n", int(getpid()));

		lockfd = ::open(name->v, O_WRONLY | O_CREAT | O_EXCL, 0666);
		if (lockfd < 0) {
			// Failed to create the file:
			if (errno == EEXIST) {
				// If the reason was that the file already exists, we did not get the lock.
				return false;
			} else {
				// Likely a permission error. We ignore these since UUCP is being phased out.
				return true;
			}
		}
		ssize_t res = ::write(lockfd, pidbuff, strlen(pidbuff));
		(void)res;
		::close(lockfd);

		lockFile = name;
#endif
		return true;
	}

	void SerialStream::releaseLock() {
		if (lockFile) {
			::unlink(lockFile->v);
			lockFile = null;
		}
	}

	MAYBE(SerialStream *) SerialPort::open(SerialOptions *options) {
		// O_NOCTTY to avoid having the serial port becoming the controlling terminal.
		os::Handle handle = ::open(identifier->utf8_str(), O_RDWR | O_NOCTTY);
		if (!handle)
			return null;

		// We need to lock the device:
		GcArray<char> *lockFile = null;
		if (!SerialStream::acquireLock(identifier, handle, lockFile)) {
			::close(handle.v());
			return null;
		}

		// Attach to the thread and continue.
		os::Thread thread = os::Thread::current();
		thread.attach(handle);

		try {
			return new (this) SerialStream(handle, thread, options, lockFile);
		} catch (...) {
			if (handle)
				::close(handle.v());
			throw;
		}
	}

	Array<SerialPort *> *serialPorts(EnginePtr e) {
		Array<SerialPort *> *result = new (e.v) Array<SerialPort *>();

		// Remember the device numbers we are looking for.
		std::set<std::pair<int, int>> devices;

		DIR *h = null;

		try {
			// Look in /sys/class to figure out which devices are serial ports.
			std::string classRoot = "/sys/class/tty/";
			h = opendir(classRoot.c_str());
			dirent *d;
			while ((d = readdir(h)) != null) {
				if (strcmp(d->d_name, "..") == 0 || strcmp(d->d_name, ".") == 0)
					continue;

				std::string path = classRoot + d->d_name;
				// Only pick those backed by a "real" device.
				struct stat statbuf;
				if (stat((path + "/device").c_str(), &statbuf) != 0)
					continue;

				FILE *f = fopen((path + "/dev").c_str(), "r");
				int maj, min;
				char sep;
				bool ok = fscanf(f, "%d%c%d", &maj, &sep, &min) == 3;
				fclose(f);

				if (ok)
					devices.insert(std::make_pair(maj, min));
			}
			closedir(h);
			h = null;

			// Enumerate all devices in /dev, read their device numbers.
			std::string devRoot = "/dev/";
			h = opendir(devRoot.c_str());
			while ((d = readdir(h)) != null) {
				if (strcmp(d->d_name, "..") == 0 || strcmp(d->d_name, ".") == 0)
					continue;
				if (d->d_type == DT_DIR)
					continue;

				std::string name = devRoot + d->d_name;
				struct stat statbuf;
				if (stat(name.c_str(), &statbuf) != 0)
					continue;

				// I think all serial ports are character devices.
				if (!S_ISCHR(statbuf.st_mode)) // && !S_ISBLK(statbuf.st_mode))
					continue;

				int maj = major(statbuf.st_rdev);
				int min = minor(statbuf.st_rdev);
				if (devices.count(std::make_pair(maj, min))) {
					// Note: Not all devices are actually enabled all the time. Typically ttySx will
					// be available, but calling tcgetattr on them will fail. So we check that here
					// already to eliminate false positives. Note: If we fail to open the port for
					// any reason (i.e. we can't check), assume it is a valid port and include it.
					bool ok = true;

					int fd = ::open(name.c_str(), O_RDONLY);
					if (fd >= 0) {
						struct termios info;
						ok = tcgetattr(fd, &info) == 0;
						::close(fd);
					}

					if (ok) {
						Str *wide = new (e.v) Str(toWChar(e.v, name.c_str()));
						result->push(new (e.v) SerialPort(wide));
					}
				}
			}
			closedir(h);
			h = null;

			return result;
		} catch (...) {
			if (h)
				closedir(h);
			throw;
		}
	}

#endif

}
