/*	Benjamin DELPY `gentilkiwi`
	http://blog.gentilkiwi.com
	benjamin@gentilkiwi.com
	Licence : https://creativecommons.org/licenses/by/4.0/
*/
#include "kull_m_busylight.h"

const BUSYLIGHT_DEVICE_ID KULL_M_BUSYLIGHT_CATALOG[] = {
	{0x27bb, 0x3bca, BUSYLIGHT_CAP_LIGHT | BUSYLIGHT_CAP_SOUND | BUSYLIGHT_CAP_JINGLE_CLIPS,	L"Busylight Lync model (with bootloader)"},
	{0x27bb, 0x3bcb, BUSYLIGHT_CAP_LIGHT | BUSYLIGHT_CAP_SOUND,									L"Busylight UC model"},
	{0x27bb, 0x3bcc, BUSYLIGHT_CAP_INPUTEVENT,													L"kuandoBOX"},
	{0x27bb, 0x3bcd, BUSYLIGHT_CAP_LIGHT | BUSYLIGHT_CAP_SOUND | BUSYLIGHT_CAP_JINGLE_CLIPS,	L"Busylight Omega model"},
	{0x04d8, 0xf848, BUSYLIGHT_CAP_LIGHT | BUSYLIGHT_CAP_SOUND,									L"Busylight Lync model (Microchip Id)"},
	{0x0bf8, 0x1020, BUSYLIGHT_CAP_LIGHT,														L"Fujitsu MMM2"},
};

const BUSYLIGHT_COLOR
	BUSYLIGHT_COLOR_OFF					= {0,	0,		0},
	BUSYLIGHT_COLOR_RED					= {100,	0,		0},
	BUSYLIGHT_COLOR_ORANGE				= {100,	50,		0},
	BUSYLIGHT_COLOR_YELLOW				= {100,	100,	0},
	BUSYLIGHT_COLOR_CHARTREUSE_GREEN	= {50,	100,	0},
	BUSYLIGHT_COLOR_GREEN				= {0,	100,	0},
	BUSYLIGHT_COLOR_SPRING_GREEN		= {0,	100,	50},
	BUSYLIGHT_COLOR_CYAN				= {0,	100,	100},
	BUSYLIGHT_COLOR_AZURE				= {0,	50,		100},
	BUSYLIGHT_COLOR_BLUE				= {0,	0,		100},
	BUSYLIGHT_COLOR_VIOLET				= {50,	0,		100},
	BUSYLIGHT_COLOR_MAGENTA				= {100,	0,		100},
	BUSYLIGHT_COLOR_ROSE				= {100,	0,		50},
	BUSYLIGHT_COLOR_WHITE				= {100, 100,	100}
;

PCBUSYLIGHT_DEVICE_ID kull_m_busylight_devices_getIdFromAttributes(PHIDD_ATTRIBUTES attributes)
{
	DWORD i;
	if(attributes)
		for(i = 0; i < ARRAYSIZE(KULL_M_BUSYLIGHT_CATALOG); i++)
			if((KULL_M_BUSYLIGHT_CATALOG[i].Vid == attributes->VendorID) && (KULL_M_BUSYLIGHT_CATALOG[i].Pid == attributes->ProductID))
				return &KULL_M_BUSYLIGHT_CATALOG[i];
	return NULL;
}

BOOL kull_m_busylight_devices_get(PBUSYLIGHT_DEVICE *devices, DWORD *count, DWORD mask, BOOL bAutoThread)
{
	PBUSYLIGHT_DEVICE *next = devices;
	GUID guidHid;
	HDEVINFO hDevInfo;
	SP_DEVICE_INTERFACE_DATA DeviceInterfaceData;
	BOOL enumStatus;
	DWORD enumIndex, dwRequired, id = 0;
	PSP_DEVICE_INTERFACE_DETAIL_DATA DeviceInterfaceDetailData;
	HANDLE deviceHandle;
	HIDD_ATTRIBUTES attributes;
	PCBUSYLIGHT_DEVICE_ID deviceId;
	PHIDP_PREPARSED_DATA PreparsedData;
	NTSTATUS status;

	*next = NULL;
	HidD_GetHidGuid(&guidHid);
	hDevInfo = SetupDiGetClassDevs(&guidHid, NULL, NULL, DIGCF_DEVICEINTERFACE | DIGCF_PRESENT);
	if(hDevInfo != INVALID_HANDLE_VALUE)
	{
		for(enumIndex = 0, enumStatus = TRUE; enumStatus; enumIndex++)
		{
			DeviceInterfaceData.cbSize = sizeof(SP_DEVICE_INTERFACE_DATA);
			if(enumStatus = SetupDiEnumDeviceInterfaces(hDevInfo, NULL, &guidHid, enumIndex, &DeviceInterfaceData))
			{
				dwRequired = 0;
				if(!SetupDiGetDeviceInterfaceDetail(hDevInfo, &DeviceInterfaceData, NULL, 0, &dwRequired, NULL) && (GetLastError() == ERROR_INSUFFICIENT_BUFFER))
				{
					if(DeviceInterfaceDetailData = (PSP_DEVICE_INTERFACE_DETAIL_DATA) LocalAlloc(LPTR, dwRequired))
					{
						DeviceInterfaceDetailData->cbSize = sizeof(SP_DEVICE_INTERFACE_DETAIL_DATA);
						if(SetupDiGetDeviceInterfaceDetail(hDevInfo, &DeviceInterfaceData, DeviceInterfaceDetailData, dwRequired, &dwRequired, NULL))
						{
							deviceHandle = CreateFile(DeviceInterfaceDetailData->DevicePath, 0, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, 0, NULL);
							if(deviceHandle != INVALID_HANDLE_VALUE)
							{
								attributes.Size = sizeof(HIDD_ATTRIBUTES);
								if(HidD_GetAttributes(deviceHandle, &attributes))
								{
									if(deviceId = kull_m_busylight_devices_getIdFromAttributes(&attributes))
									{
										if((deviceId->Capabilities & mask) == mask)
										{
											if(*next = (PBUSYLIGHT_DEVICE) LocalAlloc(LPTR, sizeof(BUSYLIGHT_DEVICE)))
											{
												if(HidD_GetPreparsedData(deviceHandle, &PreparsedData))
												{
													status = HidP_GetCaps(PreparsedData, &(*next)->hidCaps);
													if(!NT_SUCCESS(status))
														PRINT_ERROR(L"HidP_GetCaps (%08x)\n", status);
													HidD_FreePreparsedData(PreparsedData);
												}
												(*next)->DevicePath = _wcsdup(DeviceInterfaceDetailData->DevicePath);
												(*next)->hidAttributes = attributes;
												(*next)->deviceId = deviceId;
												(*next)->dpi.box_sensivity = 4;
												(*next)->dpi.box_timeout = 4;
												(*next)->dpi.box_triggertime = 85;
												(*next)->id = id;
												(*next)->hBusy = CreateFile(DeviceInterfaceDetailData->DevicePath, FILE_READ_DATA | FILE_WRITE_DATA, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, 0, NULL);
												
												if((*next)->hBusy && ((*next)->hBusy != INVALID_HANDLE_VALUE))
												{
													if(bAutoThread)
													{
														(*next)->dKeepAliveThread = 5000;
														if((*next)->hKeepAliveThread = CreateThread(NULL, 0, kull_m_busylight_keepAliveThread, *next, 0, NULL))
														{
															next = &(*next)->next;
															id++;
														}
														else
														{
															PRINT_ERROR_AUTO(L"CreateThread (hKeepAliveThread)");
															CloseHandle((*next)->hBusy);
															LocalFree(*next);
														}
													}
													else
													{
														next = &(*next)->next;
														id++;
													}
												}
												else
												{
													PRINT_ERROR_AUTO(L"CreateFile (hBusy)");
													LocalFree(*next);
												}
											}
										}
									}
								}
								CloseHandle(deviceHandle);
							}
							else PRINT_ERROR_AUTO(L"CreateFile (deviceHandle)");
						}
						LocalFree(DeviceInterfaceDetailData);
					}
				}
			}
		}
		SetupDiDestroyDeviceInfoList(hDevInfo);
	}
	else PRINT_ERROR_AUTO(L"SetupDiGetClassDevs");

	if(count)
		*count = id;
	return (id > 0);
}

void kull_m_busylight_devices_free(PBUSYLIGHT_DEVICE devices, BOOL instantOff)
{
	PBUSYLIGHT_DEVICE tmp;
	while(devices)
	{
		if(devices->hBusy)
		{
			if(instantOff && (devices->hBusy != INVALID_HANDLE_VALUE))
				kull_m_busylight_request_send_off(devices, FALSE);
			CloseHandle(devices->hBusy);
			devices->hBusy = NULL;
		}
		devices->dKeepAliveThread = 0;
		if(devices->hKeepAliveThread)
		{
			TerminateThread(devices->hKeepAliveThread, ERROR_SUCCESS);
			devices->hKeepAliveThread = NULL;
		}
		devices->dWorkerThread = 0;
		if(devices->hWorkerThread)
		{
			TerminateThread(devices->hWorkerThread, ERROR_SUCCESS);
			devices->hWorkerThread = NULL;
		}
		if(devices->DevicePath)
			free(devices->DevicePath);
		tmp = devices->next;
		LocalFree(devices);
		devices = tmp;
	}
}

BOOL kull_m_busylight_request_create(PCBUSYLIGHT_COMMAND_STEP commands, DWORD count, PBYTE *data, DWORD *size)
{
	BOOL status = FALSE;
	DWORD i;
	USHORT sum;

	*size = BUSYLIGHT_OUTPUT_REPORT_SIZE;
	if(*data = (PBYTE) LocalAlloc(LPTR, *size))
	{
		if(count >=7)
			PRINT_ERROR(L"count=%u (max is 7)\n", count);
		for(i = 0; i < min(count, 7); i++)
		{
			(*data)[i * 8 + 1] = (commands[i].NextStep & 0xf0) ? commands[i].NextStep : (commands[i].NextStep | 0x10);
			(*data)[i * 8 + 2] = commands[i].RepeatInterval;
			// TODO avoid color (or not ?)
			(*data)[i * 8 + 3] = commands[i].color.red;
			(*data)[i * 8 + 4] = commands[i].color.green;
			(*data)[i * 8 + 5] = commands[i].color.blue;

			(*data)[i * 8 + 6] = commands[i].OnTimeSteps;
			(*data)[i * 8 + 7] = commands[i].OffTimeSteps;
			(*data)[i * 8 + 8] = commands[i].AudioByte;
		}
		(*data)[57] = 4;
		(*data)[58] = 4;
		(*data)[59] = 85;

		(*data)[60] = (*data)[61] = (*data)[62] = 0xff;
		
		for(i = 1, sum = 0; i < (*size - 2); i++)
			sum += (*data)[i];
		(*data)[63] = (BYTE) (sum / 256);
		(*data)[64] = (BYTE) (sum % 256);

		status = TRUE; // TODO add checks
		if(!status)
		{
			*data = (PBYTE) LocalFree(*data);
			*size = 0;
		}
	}
	return status;
}

BOOL kull_m_busylight_device_send_raw(PBUSYLIGHT_DEVICE device, LPCVOID request, DWORD size)
{
	BOOL status = FALSE;
	DWORD writed;
	if(device && device->hBusy && (device->hBusy != INVALID_HANDLE_VALUE))
	{
		if(size <= device->hidCaps.OutputReportByteLength)
		{
			status = WriteFile(device->hBusy, request, size, &writed, NULL);
			if(!status)
				PRINT_ERROR(L"[device %u] WriteFile (0x%08x)\n", device->id, GetLastError());
		}
		else PRINT_ERROR(L"[device %u] Size is not valide (siz = %u, max = %u)\n", device->id, size, device->hidCaps.OutputReportByteLength);
	}
	else PRINT_ERROR(L"[device %u] Invalid Device/Busy Handle\n", device->id);
	return status;
}

BOOL kull_m_busylight_device_read_raw(PBUSYLIGHT_DEVICE device, LPVOID *data, DWORD *size)
{
	BOOL status = FALSE;
	DWORD toRead;
	if(device && device->hBusy && (device->hBusy != INVALID_HANDLE_VALUE))
	{
		toRead = device->hidCaps.InputReportByteLength;
		if(*data = LocalAlloc(LPTR, toRead))
		{
			status = ReadFile(device->hBusy, *data, toRead, size, NULL);
			if(!status || (status && (*size != toRead)))
			{
				if(!status)
					PRINT_ERROR(L"[device %u] ReadFile (0x%08x)\n", device->id, GetLastError());
				else
					PRINT_ERROR(L"[device %u] %u byte(s) readed, %u wanted\n", *size, toRead);
				*data = LocalFree(*data);
				*size = 0;
			}
		}
	}
	else PRINT_ERROR(L"[device %u] Invalid Device/Busy Handle\n", device->id);
	return status;
}

DWORD WINAPI kull_m_busylight_keepAliveThread(LPVOID lpThreadParameter)
{
	PBUSYLIGHT_DEVICE device = (PBUSYLIGHT_DEVICE) lpThreadParameter;
	while(device && device->hKeepAliveThread && device->dKeepAliveThread && device->hBusy)
	{
		if(kull_m_busylight_request_send_keepalive(device, FALSE))
			Sleep(device->dKeepAliveThread);
		else
		{
			CloseHandle(device->hBusy);
			device->hBusy = NULL;
			device->dKeepAliveThread = 0;
			device->hKeepAliveThread = NULL;
			break;
		}
	};
	return ERROR_SUCCESS;
}

BOOL kull_m_busylight_device_read_infos(PBUSYLIGHT_DEVICE device, BUSYLIGHT_INFO *info)
{
	BOOL status = FALSE;
	LPBYTE data;
	DWORD size;
	if(kull_m_busylight_request_send_keepalive(device, FALSE))
	{
		if(kull_m_busylight_device_read_raw(device, (LPVOID *) &data, &size))
		{
			RtlZeroMemory(info, sizeof(BUSYLIGHT_INFO));
			if(status = !data[0])
			{
				info->status = data[1];
				RtlCopyMemory(info->ProductId, data + 2, 3);
				RtlCopyMemory(info->CostumerId, data + 5, 8);
				RtlCopyMemory(info->Model, data + 13, 4);
				RtlCopyMemory(info->Serial, data + 17, 8);
				RtlCopyMemory(info->Mfg_ID, data + 25, 8);
				RtlCopyMemory(info->Mfg_Date, data + 33, 8);
				RtlCopyMemory(info->swrelease, data + 41, 6);
			}
			else PRINT_ERROR(L"[device %u] data[0] is not NULL (0x%02x)\n", data[0]);
			LocalFree(data);
		}
	}
	return status;
}

BOOL kull_m_busylight_request_send(PBUSYLIGHT_DEVICE device, PCBUSYLIGHT_COMMAND_STEP commands, DWORD count, BOOL all)
{
	BOOL status = FALSE;
	PBUSYLIGHT_DEVICE cur;
	LPBYTE data;
	DWORD size;
	if(status = kull_m_busylight_request_create(commands, count, &data, &size))
	{
		for(cur = device; cur; cur = all ? cur->next : NULL)
			status &= kull_m_busylight_device_send_raw(cur, data, size);
		LocalFree(data);
	}
	return status;
}

BOOL kull_m_busylight_request_send_keepalive(PBUSYLIGHT_DEVICE device, BOOL all)
{
	BUSYLIGHT_COMMAND_STEP mdl = {0x8f, 0, {0, 0, 0}, 0, 0, 0};
	return kull_m_busylight_request_send(device, &mdl, 1, all);
}

BOOL kull_m_busylight_request_send_off(PBUSYLIGHT_DEVICE device, BOOL all)
{
	PBUSYLIGHT_DEVICE cur;
	BUSYLIGHT_COMMAND_STEP mdl = {0, 0, {0, 0, 0}, 0, 0, BUSYLIGHT_MEDIA_MUTE};

	for(cur = device; cur; cur = all ? cur->next : NULL)
	{
		cur->dWorkerThread = 0;
		if(cur->hWorkerThread)
		{
			TerminateThread(cur->hWorkerThread, ERROR_SUCCESS);
			cur->hWorkerThread = NULL;
		}
	}
	return kull_m_busylight_request_send(device, &mdl, 1, all);
}

BOOL kull_m_busylight_request_single_send(PBUSYLIGHT_DEVICE device, const BUSYLIGHT_COLOR * color, BYTE sound, BYTE volume, BOOL all)
{
	BUSYLIGHT_COMMAND_STEP mdl = {0, 0, {color->red, color->green, color->blue}, 0, 0, BUSYLIGHT_MEDIA(sound, volume)};
	return kull_m_busylight_request_send(device, &mdl, 1, all);
}