#include <termios.h>

#include "nfn.h"

namespace serial {

	using namespace nfn;

	struct serial_device : public nfn_device {
		speed_t baud{B9600};
		int size{CS8};
		uint8_t parity{0};
		uint8_t stop{1};
		bool hw{false};
		bool sw{false};
	};

	class serial_nfn : public nfn_Core {
		protected:
			Local<String> info_get(Isolate *isolate, nfn_device *dev) {
				serial_device *info = (serial_device *) dev;
				char str[16];
				size_t i = 0;

				memset(&str[0], 0, 16);

				switch (info->baud) {
					case B2400:
						i = 5;
						memcpy(&str[0], "2400_", i);
						break;
					case B4800:
						i = 5;
						memcpy(&str[0], "4800_", i);
						break;
					case B9600:
					default:
						i = 5;
						memcpy(&str[0], "9600_", i);
						break;
					case B19200:
						i = 6;
						memcpy(&str[0], "19200_", i);
						break;
					case B38400:
						i = 6;
						memcpy(&str[0], "38400_", i);
						break;
					case B57600:
						i = 6;
						memcpy(&str[0], "57600_", i);
						break;
					case B115200:
						i = 7;
						memcpy(&str[0], "115200_", i);
						break;
				}

				switch (info->size) {
					case CS5:
						memcpy(&str[i], "5_", 2);
						i += 2;
						break;
					case CS6:
						memcpy(&str[i], "6_", 2);
						i += 2;
						break;
					case CS7:
						memcpy(&str[i], "7_", 2);
						i += 2;
						break;
					case CS8:
					default:
						memcpy(&str[i], "8_", 2);
						i += 2;
						break;
				}

				switch (info->parity) {
					case 1:
						memcpy(&str[i], "ODD_", 4);
						i += 4;
						break;
					case 2:
						memcpy(&str[i], "EVEN_", 5);
						i += 5;
						break;
					case 0:
					default:
						memcpy(&str[i], "NONE_", 5);
						i += 5;
						break;
				}

				memcpy(&str[i], info->stop == 2 ? "2" : "1", 1);

				return String::NewFromUtf8(isolate, &str[0]);
			}

			nfn_device *info_set_custom(Isolate *isolate, Local<Object> opt) {
				serial_device *dev = new serial_device;
				Local<String> key[] = {
						String::NewFromUtf8(isolate, "speed"),
						String::NewFromUtf8(isolate, "size"),
						String::NewFromUtf8(isolate, "parity"),
						String::NewFromUtf8(isolate, "stop"),
						String::NewFromUtf8(isolate, "hw"),
						String::NewFromUtf8(isolate, "sw")
				};
				Local<Value> vlv;
				size_t vli;

				vlv = opt->Get(key[0]);
				if (vlv->IsNumber()) {
					vli = (size_t) vlv->NumberValue();
					switch (vli) {
						case 2400:
							dev->baud = B2400;
							break;
						case 4800:
							dev->baud = B4800;
							break;
						case 9600:
						default:
							dev->baud = B9600;
							break;
						case 19200:
							dev->baud = B19200;
							break;
						case 38400:
							dev->baud = B38400;
							break;
						case 57600:
							dev->baud = B57600;
							break;
						case 115200:
							dev->baud = B115200;
							break;
					}
				}

				vlv = opt->Get(key[1]);
				if (vlv->IsNumber()) {
					vli = (size_t) vlv->NumberValue();
					switch (vli) {
						case 5:
							dev->size = CS5;
							break;
						case 6:
							dev->size = CS6;
							break;
						case 7:
							dev->size = CS7;
							break;
						case 8:
						default:
							dev->size = CS8;
							break;
					}
				}

				vlv = opt->Get(key[2]);
				if (vlv->IsNumber()) dev->parity = (uint8_t) vlv->NumberValue();

				vlv = opt->Get(key[3]);
				if (vlv->IsNumber()) dev->stop = (uint8_t) (vlv->NumberValue() == 2 ? 2 : 1);

				vlv = opt->Get(key[4]);
				if (vlv->IsBoolean()) dev->hw = vlv->BooleanValue();

				vlv = opt->Get(key[5]);
				if (vlv->IsBoolean()) dev->sw = vlv->BooleanValue();

				return dev;
			}

			bool device_set(nfn_device *dev) {
				serial_device *info = (serial_device *) dev;

				struct termios opt;
				tcgetattr(info->hwnd, &opt);

				cfsetispeed(&opt, info->baud);
				cfsetospeed(&opt, info->baud);

				opt.c_cflag |= (CLOCAL | CREAD);

				opt.c_cflag &= (~CSIZE);
				opt.c_cflag |= info->size;

				switch (info->parity) {
					case 1:
						opt.c_cflag |= PARENB;
						opt.c_cflag |= PARODD;
						opt.c_iflag |= (INPCK | ISTRIP);
						break;
					case 2:
						opt.c_cflag |= PARENB;
						opt.c_cflag &= (~PARODD);
						opt.c_iflag |= (INPCK | ISTRIP);
						break;
					default:
						opt.c_cflag &= (~PARENB);
						break;
				}

				if (info->stop == 2) opt.c_cflag |= CSTOPB;
				else opt.c_cflag &= (~CSTOPB);

				//CNEW_RTSCTS / CRTSCTS
				if (info->hw) opt.c_cflag |= CRTSCTS;
				else opt.c_cflag &= ~CRTSCTS;

				if (info->sw) opt.c_iflag |= (IXON | IXOFF | IXANY);
				else opt.c_iflag &= ~(IXON | IXOFF | IXANY);

				opt.c_cc[VTIME] = 0;
				opt.c_cc[VMIN] = 0;

				tcflush(info->hwnd, TCIFLUSH);
				tcsetattr(info->hwnd, TCSANOW, &opt);

				return true;
			}

			int device_open(nfn_device *dev) {
				return open(dev->name, O_RDWR | O_NOCTTY | O_NDELAY);
			}
	};

//	api functions

	void openSerial(const FunctionCallbackInfo<Value> &arg) {
		Isolate *isolate = arg.GetIsolate();
		long hwnd = 0;

		if (arg.Length() == 2 && arg[0]->IsObject() && arg[1]->IsFunction()) {
			serial_nfn *ser = new serial_nfn();

			if (ser->start(isolate, Local<Object>::Cast(arg[0]), Local<Function>::Cast(arg[1]))) hwnd = (long) ser;
		}

		arg.GetReturnValue().Set(Number::New(isolate, hwnd));
	}

	void closeSerial(const FunctionCallbackInfo<Value> &arg) {
		if (arg.Length() != 1 || !arg[0]->IsNumber()) return;

		long hwnd = (long) Local<Number>::Cast(arg[0])->NumberValue();

		if (hwnd > 0) ((serial_nfn *) hwnd)->stop();
	}

	void resetSerial(const FunctionCallbackInfo<Value> &arg) {
		if (arg.Length() != 1 || !arg[0]->IsNumber()) return;

		long hwnd = (long) Local<Number>::Cast(arg[0])->NumberValue();

		if (hwnd > 0) ((serial_nfn *) hwnd)->reset();
	}

	void configSerial(const FunctionCallbackInfo<Value> &arg) {
		Isolate *isolate = arg.GetIsolate();
		bool result = false;

		if (arg.Length() == 2 && arg[0]->IsNumber() && arg[1]->IsObject()) {
			long hwnd = (long) Local<Number>::Cast(arg[0])->NumberValue();

			if (hwnd > 0) result = ((serial_nfn *) hwnd)->config(isolate, Local<Object>::Cast(arg[1]));
		}

		arg.GetReturnValue().Set(Boolean::New(isolate, result));
	}

	void clearData(const FunctionCallbackInfo<Value> &arg) {
		if (arg.Length() != 1 || !arg[0]->IsNumber()) return;

		long hwnd = (long) Local<Number>::Cast(arg[0])->NumberValue();

		if (hwnd > 0) ((serial_nfn *) hwnd)->clear();
	}

	void readData(const FunctionCallbackInfo<Value> &arg) {
		Isolate *isolate = arg.GetIsolate();
		bool result = false;

		if (arg.Length() == 2 && arg[0]->IsNumber() && arg[1]->IsNumber()) {
			long hwnd = (long) Local<Number>::Cast(arg[0])->NumberValue();

			if (hwnd > 0) result = ((serial_nfn *) hwnd)->read((size_t) Local<Number>::Cast(arg[1])->NumberValue());
		}

		arg.GetReturnValue().Set(Boolean::New(isolate, result));
	}

	void writeData(const FunctionCallbackInfo<Value> &arg) {
		Isolate *isolate = arg.GetIsolate();
		bool result = false;

		if (arg.Length() == 2 && arg[0]->IsNumber() && (arg[1]->IsString() || arg[1]->IsUint8Array())) {
			long hwnd = (long) Local<Number>::Cast(arg[0])->NumberValue();

			if (hwnd > 0) result = ((serial_nfn *) hwnd)->write(arg[1]);
		}

		arg.GetReturnValue().Set(Boolean::New(isolate, result));
	}

	void Initialize(Handle<Object> exports, Handle<Object> module) {
		NODE_SET_METHOD(exports, "open", openSerial);
		NODE_SET_METHOD(exports, "close", closeSerial);
		NODE_SET_METHOD(exports, "reset", resetSerial);
		NODE_SET_METHOD(exports, "config", configSerial);
		NODE_SET_METHOD(exports, "clear", clearData);
		NODE_SET_METHOD(exports, "read", readData);
		NODE_SET_METHOD(exports, "write", writeData);
	}

	NODE_MODULE(serial, Initialize
	)
}