#include "pihidevice.h"

#include "piliterals_string.h"
#include "piliterals_time.h"
#ifndef WINDOWS
#  include "pidir.h"
#  include "pifile.h"
#  include "piiostream.h"

#  include <fcntl.h>
#  include <linux/input-event-codes.h>
#  include <linux/input.h>
#  include <sys/ioctl.h>
#  include <sys/time.h>
#  include <unistd.h>
#else
// clang-format off
#  undef _WIN32_WINNT
#  define _WIN32_WINNT 0x0600
#  include <windows.h>
#  include <setupapi.h>
extern "C" {
#  include <hidsdi.h>
}
// clang-format on
#endif


bool PIHIDeviceInfo::match(const PIString & str) const {
	if (product.toLowerCase().contains(str.toLowerCase())) return true;
	if (path.toLowerCase().contains(str.toLowerCase())) return true;
	return false;
}


int PIHIDeviceInfo::axesAbsoluteCount() const {
	int ret = 0;
	for (const auto & a: axes)
		if (!a.is_relative) ++ret;
	return ret;
}


int PIHIDeviceInfo::axesRelativeCount() const {
	int ret = 0;
	for (const auto & a: axes)
		if (a.is_relative) ++ret;
	return ret;
}


void PIHIDeviceInfo::prepare() {
	axis_by_dataindex.clear();
	button_by_dataindex.clear();
	for (const auto & i: axes)
		axis_by_dataindex[i.data_index] = i;
	for (const auto & i: buttons)
		button_by_dataindex[i.data_index] = i;
}


PICout operator<<(PICout s, const PIHIDeviceInfo & v) {
	s.saveAndSetControls(0);
	s << "PIHIDeviceInfo(" << v.product << " (" << v.manufacturer << "), " << v.VID << ":" << v.PID
	  << ", " //<< "path \"" << v.path << "\", "
	  << v.axesAbsoluteCount() << " abs axes, " << v.axesRelativeCount() << " rel axes, " << v.buttonsCount() << " buttons)";
	s.restoreControls();
	return s;
}


PRIVATE_DEFINITION_START(PIHIDevice)
#ifndef WINDOWS
	PIFile file;
	bool is_js = false;
#else
	PIByteArray buffer;
	HANDLE deviceHandle            = nullptr;
	PHIDP_PREPARSED_DATA preparsed = nullptr;
#endif
PRIVATE_DEFINITION_END(PIHIDevice)


PIHIDevice::~PIHIDevice() {
	close();
}

bool PIHIDevice::isOpened() const {
#ifndef WINDOWS
	return PRIVATE->file.isOpened();
#else
	return PRIVATE->deviceHandle;
#endif
}


bool PIHIDevice::open(const PIHIDeviceInfo & device) {
	close();
	cur_axes.clear();
	cur_buttons.clear();
	di = device;
	di.prepare();
	if (device.isNull()) return false;
#ifndef WINDOWS
	if (!PRIVATE->file.open(di.path, PIIODevice::ReadOnly)) {
		piCout << "PIHIDevice::open" << di.path << "error:" << errorString();
		return false;
	}
	PRIVATE->is_js = PIFile::FileInfo(di.path).name().startsWith("js"_a);
	return true;
#else
	PRIVATE->deviceHandle = CreateFileA(di.path.dataAscii(),
										GENERIC_READ | GENERIC_WRITE,
										FILE_SHARE_READ | FILE_SHARE_WRITE,
										nullptr,
										OPEN_EXISTING,
										0,
										nullptr);
	if (PRIVATE->deviceHandle == INVALID_HANDLE_VALUE) {
		piCoutObj << "PIHIDevice::open" << di.path << "error:" << errorString();
		PRIVATE->deviceHandle = nullptr;
		return false;
	}
	HidD_GetPreparsedData(PRIVATE->deviceHandle, &PRIVATE->preparsed);
	if (!PRIVATE->preparsed) {
		close();
		return false;
	}
	return true;
#endif
}


bool PIHIDevice::open() {
	return open(di);
}


void PIHIDevice::close() {
	stop();
#ifndef WINDOWS
	PRIVATE->file.close();
#else
	if (PRIVATE->deviceHandle) {
		CloseHandle(PRIVATE->deviceHandle);
		PRIVATE->deviceHandle = nullptr;
	}
	if (PRIVATE->preparsed) {
		HidD_FreePreparsedData(PRIVATE->preparsed);
		PRIVATE->preparsed = nullptr;
	}
#endif
}


void PIHIDevice::start() {
	if (!isOpened()) return;
	PIThread::start(200_Hz);
#ifndef WINDOWS
#else
#endif
}


void PIHIDevice::stop() {
	PIThread::stop();
#ifdef WINDOWS
	if (PRIVATE->deviceHandle) {
		CancelIoEx(PRIVATE->deviceHandle, nullptr);
	}
#endif
	if (!waitForFinish(1000_ms)) terminate();
}


void PIHIDevice::run() {
	Event e;
#ifndef WINDOWS
#  pragma pack(push, 1)
	struct input_event {
		struct timeval time;
		ushort type;
		ushort code;
		uint value;
	};
	struct js_event {
		uint time;    /* event timestamp in milliseconds */
		short value;  /* value */
		uchar type;   /* event type */
		uchar number; /* axis/button number */
	};
#  pragma pack(pop)
	if (PRIVATE->is_js) {
		js_event ie;
		while (PRIVATE->file.read(&ie, sizeof(ie)) == sizeof(ie)) {
			if (ie.type == 0) continue;
			bool ok = false;
			switch (ie.type) {
			case 2: {
				// piCout << ie.value;
				cur_axes[ie.number] = procDeadZone(ie.value / 32767. / 2. + 0.5);
				ok                  = true;
			} break;
			case 1:
				// piCout << ie.code;
				cur_buttons[ie.number] = ie.value;
				ok                     = true;
				break;
			default: break;
			}
			// piCout << ok << ie.type << ie.code << ie.value;
			if (!ok) continue;
		}
	} else {
		input_event ie;
		while (PRIVATE->file.read(&ie, sizeof(ie)) == sizeof(ie)) {
			if (ie.type == 0) continue;
			bool ok = false;
			switch (ie.type) {
			case 2: { // rel axis
				auto vi = di.axis_by_dataindex.value(ie.code);
				if (vi.isValid()) {
					e.type  = Event::tAxisMove;
					e.axis  = vi;
					e.value = static_cast<int>(ie.value);
					event(e);
				}
				ok = true;
			} break;
			case 3: { // abs axis
				auto vi           = di.axis_by_dataindex.value(ie.code);
				float fv          = (ie.value - vi.min) / piMaxf(1.f, (float)(vi.max - vi.min));
				cur_axes[ie.code] = procDeadZone(fv);
				ok                = true;
			} break;
			case 1: // button
				// piCout << ie.code;
				cur_buttons[ie.code] = ie.value;
				ok                   = true;
				break;
			default: break;
			}
			// piCout << ok << ie.type << ie.code << ie.value;
			if (!ok) continue;
		}
	}
#else
	PRIVATE->buffer.resize(di.input_report_size).fill(0);
	DWORD readed = 0;
	// piCout << "read" << PRIVATE->deviceHandle << PRIVATE->buffer.size();
	HIDP_DATA gdd[256];
	ULONG gdd_len = 256;
	if (ReadFile(PRIVATE->deviceHandle, PRIVATE->buffer.data(), PRIVATE->buffer.size_s(), &readed, nullptr) != TRUE) return;
	// piCout << readed << PRIVATE->buffer.size();
	if (readed != PRIVATE->buffer.size()) return;
	auto gd = HidP_GetData(HidP_Input, gdd, &gdd_len, PRIVATE->preparsed, (PCHAR)PRIVATE->buffer.data(), PRIVATE->buffer.size_s());
	NO_UNUSED(gd);
	// piCout << "readed" << PRIVATE->buffer << gdd_len;
	auto cbit = cur_buttons.makeIterator();
	while (cbit.next())
		cbit.value() = 0;
	for (ULONG i = 0; i < gdd_len; ++i) {
		const auto & cd(gdd[i]);
		// piCout << cd.DataIndex << cd.RawValue;
		auto vi = di.axis_by_dataindex.value(cd.DataIndex);
		if (vi.isValid()) {
			if (vi.is_relative) {
				e.type  = Event::tAxisMove;
				e.axis  = vi;
				e.value = static_cast<LONG>(cd.RawValue);
				event(e);
			} else {
				// auto & axis(cur_axes[cd.DataIndex]);
				float fv                = (cd.RawValue - vi.min) / piMaxf(1.f, (float)(vi.max - vi.min));
				cur_axes[vi.data_index] = procDeadZone(fv);
			}
			// piCout << "axis" << vi.data_index << "->" << cur_axes[vi.data_index];
			continue;
		}
		auto bi = di.button_by_dataindex.value(cd.DataIndex);
		if (bi.isValid()) {
			cur_buttons[bi.data_index] = 1;
			// piCout << "button" << bi.data_index << "-> 1";
			continue;
		}
	}
#endif

	auto ait = cur_axes.makeIterator();
	e.type   = Event::tAxisMove;
	while (ait.next()) {
		if (ait.value() != prev_axes.value(ait.key())) {
			e.axis  = di.axis_by_dataindex.value(ait.key());
			e.value = ait.value();
			event(e);
		}
	}
	prev_axes = cur_axes;

	auto bit  = cur_buttons.makeIterator();
	e.type    = Event::tButton;
	while (bit.next()) {
		if (bit.value() != prev_buttons.value(bit.key())) {
			e.button = di.button_by_dataindex.value(bit.key());
			e.value  = bit.value();
			event(e);
		}
	}
	prev_buttons = cur_buttons;
}


double PIHIDevice::procDeadZone(double in) {
	double cv = (in - 0.5) * 2.;
	if (piAbsd(cv) < dead_zone) return 0.;
	if (cv < 0)
		return (cv + dead_zone) / (1. - dead_zone);
	else
		return (cv - dead_zone) / (1. - dead_zone);
	return cv;
}


PIVector<PIHIDeviceInfo> PIHIDevice::allDevices(bool try_open) {
	PIVector<PIHIDeviceInfo> ret;

#ifndef WINDOWS

	auto readFile = [](const PIString & path) {
		auto ba = PIFile::readAll(path);
		PIString ret;
		for (const auto & b: ba) {
			if (!PIChar(b).isAscii()) break;
			ret += PIChar(b);
		}
		return ret.trim();
	};
	auto isDir = [](const PIFile::FileInfo & fi) {
		return fi.isDir() && !fi.flags[PIFile::FileInfo::Dot] && !fi.flags[PIFile::FileInfo::DotDot];
	};
	auto checkBit = [](const ullong & flags, ullong bit, const PIString & name) { return (flags & (1ULL << bit)) > 0; };

	PIDir hid_dir("/sys/bus/hid/devices"_a);
	auto hid_devs = hid_dir.entries();
	for (const auto & hd: hid_devs) {
		// piCout << d.path;
		if (!isDir(hd)) continue;
		PIDir dir_input(hd.path + "/input"_a);
		auto hid_inputs = dir_input.entries();
		for (const auto & hd_i: hid_inputs) {
			if (!isDir(hd_i)) continue;
			// now in /sys/bus/hid/devices/<dev>/input/input<N>
			// piCout << hd_i.path;
			PIHIDeviceInfo dev;
			dev.product           = readFile(hd_i.path + "/name"_a);
			// piCout << readFile(hd_i.path + "/name"_a);
			dev.VID               = readFile(hd_i.path + "/id/vendor"_a);
			dev.PID               = readFile(hd_i.path + "/id/product"_a);
			dev.version           = readFile(hd_i.path + "/id/version"_a);
			dev.manufacturer      = readFile(hd_i.path + "/id/manufacturer"_a);

			// piCout << dev.product;
			dev.input_report_size = 24;
			PIDir dir_e(hd_i.path);
			PIStringList devs;
			auto dl_e = dir_e.entries();
			for (const auto & d_e: dl_e) {
				if (!d_e.isDir() || d_e.flags[PIFile::FileInfo::Dot] || d_e.flags[PIFile::FileInfo::DotDot]) continue;
				devs << d_e.name();
			}
			/*bool dev_found = false;
			for (const auto & d: devs) {
				if (d.startsWith("js"_a)) {
					dev.path  = "/dev/input/"_a + d;
					dev_found = true;
					break;
				}
			}
			if (!dev_found) {*/
			// search for event<N> dir
			for (const auto & d: devs) {
				if (d.startsWith("event"_a)) {
					dev.path = "/dev/input/"_a + d;
					break;
				}
			}

			if (dev.path.isEmpty()) continue;

			if (try_open) {
				PIFile test_f(dev.path, PIIODevice::ReadOnly);
				if (test_f.isClosed()) continue;
			}

			// ullong ev     = readFile(hd_i.path + "/capabilities/ev"_a).toULLong(16);

			auto readAxes = [readFile, checkBit, &hd_i, &dev](const PIString & file, bool is_relative) {
				PIVector<PIHIDeviceInfo::AxisInfo> ret;
				ullong bits = readFile(hd_i.path + file).toULLong(16);
				// piCout<< PICoutManipulators::Bin << abs;
				if (bits > 0) {
					int fd = ::open(dev.path.dataAscii(), O_RDONLY);
					if (fd < 0) {
						// piCout << "Warning: can`t open" << dev.path << errorString();
					}
					PIHIDeviceInfo::AxisInfo ai;
					ai.is_relative = is_relative;
					for (int bit = 0; bit < 64; ++bit) {
						if (checkBit(bits, bit, PIString::fromNumber(bit))) {
							ai.data_index = bit;
							if (fd >= 0) {
								struct input_absinfo abs_info;
								if (ioctl(fd, EVIOCGABS(bit), &abs_info) != -1) {
									ai.min = abs_info.minimum;
									ai.max = abs_info.maximum;
									// piCout << "axis" << bit << abs_info.minimum << abs_info.maximum << abs_info.flat << abs_info.fuzz;
								} else {
									ai.min = 0;
									ai.max = 1024;
								}
							}
							ret << ai;
						}
					}
					if (fd >= 0) ::close(fd);
				}
				return ret;
			};

			dev.axes << readAxes("/capabilities/abs"_a, false) << readAxes("/capabilities/rel"_a, true);
			for (int i = 0; i < dev.axes.size_s(); ++i)
				dev.axes[i].index = i;

			PIString key_str = readFile(hd_i.path + "/capabilities/key"_a);
			PIVector<ulong> key_words;
			while (key_str.isNotEmpty()) {
				PIString w = key_str.takeWord().trimmed();
				if (w.isEmpty()) break;
				key_words.prepend(w.toULong(16));
			}
			PIBitArray key_bits((const uchar *)key_words.data(), key_words.size_s() * sizeof(ulong));

			PIHIDeviceInfo::ButtonInfo bi;
			for (uint b = 0; b < key_bits.bitSize(); ++b) {
				if (!key_bits[b]) continue;
				bi.index      = dev.buttons.size_s();
				bi.code       = b;
				bi.data_index = b;
				dev.buttons << bi;
			}

			if (dev.manufacturer.isEmpty()) {
				for (const auto * hwp: {"/usr/share/hwdata/usb.ids", "/var/lib/usbutils/usb.ids"}) {
					PIFile hwf(hwp, PIIODevice::ReadOnly);
					if (hwf.isClosed()) continue;
					// piCout << "search" << dev.VID << "in" << hwp;
					PIString line;
					PIIOTextStream ts(&hwf);
					while (!hwf.isEnd()) {
						line = ts.readLine();
						if (line.startsWith(dev.VID)) {
							line.takeWord();
							line.trim();
							dev.manufacturer = line;
							break;
						}
					}
					break;
				}
			}

			if (dev.isNotNull() && (dev.buttonsCount() > 0 || dev.axesCount() > 0)) ret << dev;
		}
	}

#else

	GUID guid;
	HidD_GetHidGuid(&guid);
	HDEVINFO deviceInfoSet = SetupDiGetClassDevs(&guid, nullptr, nullptr, DIGCF_PRESENT | DIGCF_DEVICEINTERFACE);
	if (deviceInfoSet == INVALID_HANDLE_VALUE) {
		piCout << "SetupDiGetClassDevs error:" << errorString();
		return ret;
	}

	SP_DEVICE_INTERFACE_DATA deviceInterfaceData;
	deviceInterfaceData.cbSize = sizeof(SP_DEVICE_INTERFACE_DATA);
	for (DWORD i = 0; SetupDiEnumDeviceInterfaces(deviceInfoSet, nullptr, &guid, i, &deviceInterfaceData); ++i) {
		PIHIDeviceInfo dev;

		DWORD requiredSize = 0;
		SetupDiGetDeviceInterfaceDetail(deviceInfoSet, &deviceInterfaceData, nullptr, 0, &requiredSize, nullptr);
		PSP_DEVICE_INTERFACE_DETAIL_DATA deviceInterfaceDetailData =
			reinterpret_cast<PSP_DEVICE_INTERFACE_DETAIL_DATA>(new BYTE[requiredSize]);
		PIScopeExitCall exit_call([&deviceInterfaceDetailData]() { delete[] reinterpret_cast<BYTE *>(deviceInterfaceDetailData); });
		deviceInterfaceDetailData->cbSize = sizeof(SP_DEVICE_INTERFACE_DETAIL_DATA);
		if (!SetupDiGetDeviceInterfaceDetail(deviceInfoSet,
											 &deviceInterfaceData,
											 deviceInterfaceDetailData,
											 requiredSize,
											 nullptr,
											 nullptr)) {
			piCout << "SetupDiGetDeviceInterfaceDetail error:" << errorString();
			continue;
		}

		if (try_open) {
			auto test_f = CreateFileA(deviceInterfaceDetailData->DevicePath,
									  GENERIC_READ | GENERIC_WRITE,
									  FILE_SHARE_READ | FILE_SHARE_WRITE,
									  nullptr,
									  OPEN_EXISTING,
									  0,
									  nullptr);
			if (test_f == INVALID_HANDLE_VALUE) continue;
			CloseHandle(test_f);
		}

		HANDLE deviceHandle =
			CreateFileA(deviceInterfaceDetailData->DevicePath, 0, FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, OPEN_EXISTING, 0, nullptr);
		if (deviceHandle == INVALID_HANDLE_VALUE) {
			piCout << "CreateFileA error:" << errorString();
			continue;
		}

		// piCout << i << deviceHandle;

		PHIDP_PREPARSED_DATA preparsed = nullptr;
		if (HidD_GetPreparsedData(deviceHandle, &preparsed) == FALSE) {
			piCout << "HidD_GetPreparsedData error:" << errorString();
			CloseHandle(deviceHandle);
			continue;
		}
		// auto pp = PIByteArray(preparsed, 64);
		// piCout << piChangedEndian(pp.dataAs<ushort>(7)) << piChangedEndian(pp.dataAs<ushort>(8)) <<
		// piChangedEndian(pp.dataAs<ushort>(9));
		// piCout << pp;

		dev.path = deviceInterfaceDetailData->DevicePath;

		HIDD_ATTRIBUTES attr;
		PIByteArray str_buff(1024);
		HidD_GetAttributes(deviceHandle, &attr);
		dev.VID.setNumber(attr.VendorID, 16).expandLeftTo(4, '0').toLowerCase();
		dev.PID.setNumber(attr.ProductID, 16).expandLeftTo(4, '0').toLowerCase();
		dev.version.setNumber(attr.VersionNumber);

		HIDP_CAPS caps;
		HidP_GetCaps(preparsed, &caps);
		dev.input_report_size = caps.InputReportByteLength;

		str_buff.fill(0);
		HidD_GetManufacturerString(deviceHandle, str_buff.data(), str_buff.size_s());
		dev.manufacturer = PIString(reinterpret_cast<wchar_t *>(str_buff.data()));

		str_buff.fill(0);
		HidD_GetProductString(deviceHandle, str_buff.data(), str_buff.size_s());
		dev.product = PIString(reinterpret_cast<wchar_t *>(str_buff.data()));

		str_buff.fill(0);
		HidD_GetSerialNumberString(deviceHandle, str_buff.data(), str_buff.size_s());
		dev.serial = PIString(reinterpret_cast<wchar_t *>(str_buff.data()));

		PIVector<PIHIDeviceInfo::AxisInfo> axes_abs, axes_rel;
		HIDP_VALUE_CAPS value_caps[1024];
		USHORT _cnt = caps.NumberInputValueCaps;
		memset(value_caps, 0, sizeof(value_caps[0]) * 1024);
		HidP_GetValueCaps(HidP_Input, value_caps, &_cnt, preparsed);
		for (int i = 0; i < caps.NumberInputValueCaps; ++i) {
			const auto & vc(value_caps[i]);
			PIHIDeviceInfo::AxisInfo vi;
			vi.bits        = vc.BitSize;
			vi.min         = vc.LogicalMin;
			vi.max         = vc.LogicalMax;
			vi.is_relative = vc.IsAbsolute == 0;
			if (vi.max == vi.min) ++vi.max;
			if (vc.IsRange == 1) {
				int count     = vc.Range.UsageMax - vc.Range.UsageMin + 1;
				int cur_index = vc.Range.DataIndexMin;
				for (int v = 0; v < count; ++v) {
					vi.data_index = cur_index;
					if (vi.is_relative)
						axes_rel << vi;
					else
						axes_abs << vi;
					++cur_index;
				}
			} else {
				vi.data_index = vc.NotRange.DataIndex;
				if (vi.is_relative)
					axes_rel << vi;
				else
					axes_abs << vi;
			}
			// piCout << vc.LinkCollection << vc.LinkUsage << vc.LinkUsagePage;
		}
		dev.axes << axes_abs << axes_rel;
		for (int i = 0; i < dev.axes.size_s(); ++i)
			dev.axes[i].index = i;

		HIDP_BUTTON_CAPS button_caps[1024];
		_cnt = caps.NumberInputButtonCaps;
		memset(button_caps, 0, sizeof(button_caps[0]) * 1024);
		HidP_GetButtonCaps(HidP_Input, button_caps, &_cnt, preparsed);
		for (int i = 0; i < _cnt; ++i) {
			const auto & bc(button_caps[i]);
			PIHIDeviceInfo::ButtonInfo bi;
			// dev.values.append(PIHIDeviceInfo::ValueInfo{value_caps[i].BitSize, value_caps[i].LogicalMin, value_caps[i].LogicalMax});
			if (bc.IsRange == 1) {
				int count     = bc.Range.UsageMax - bc.Range.UsageMin + 1;
				int cur_index = bc.Range.DataIndexMin;
				for (int b = 0; b < count; ++b) {
					bi.index      = dev.buttons.size_s();
					bi.data_index = cur_index;
					dev.buttons << bi;
					++cur_index;
					// piCout << b << (start_bit / 8) << (start_bit % 8);
				}
			} else {
				bi.index      = dev.buttons.size_s();
				bi.data_index = bc.NotRange.DataIndex;
				dev.buttons << bi;
				// piCout << (bi.bit / 8) << (bi.bit % 8);
			}
			// piCout << bc.IsRange << bc.Range.UsageMin << bc.Range.UsageMax
			// 	   << bc.Range.DataIndexMin << bc.Range.DataIndexMax;
		}

		HidD_FreePreparsedData(preparsed);
		CloseHandle(deviceHandle);

		if (dev.buttonsCount() > 0 || dev.axesCount() > 0) ret << dev;
	}

	SetupDiDestroyDeviceInfoList(deviceInfoSet);

#endif

	return ret;
}


PIHIDeviceInfo PIHIDevice::findDevice(const PIString & name) {
	if (name.isEmpty()) return PIHIDeviceInfo();
	auto devices = PIHIDevice::allDevices();
	for (auto d: devices) {
		if (d.match(name)) return d;
	}
	return PIHIDeviceInfo();
}
