#ifndef _WIN32
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#else
# ifndef  _CRT_SECURE_NO_WARNINGS
#  define  _CRT_SECURE_NO_WARNINGS
# endif
#include <WinSock2.h>
#pragma comment(lib, "ws2_32.lib")
#endif

#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <stdint.h>
#include <stdlib.h>
#include <thread>
#include <vector>
#include <mutex>
#include <string>
#include <algorithm>
#include <chrono>
#include <unordered_map>
#include <map>

#include <event2/listener.h>
#include <event2/buffer.h>
#include <event2/bufferevent.h>
#include <event2/thread.h>

#if ENABLE_BREAKPAD 
#ifdef _WIN32
#include <client/windows/handler/exception_handler.h>
static bool dumpCallback(const wchar_t* dump_path,
						 const wchar_t* minidump_id,
						 void* context,
						 EXCEPTION_POINTERS* exinfo,
						 MDRawAssertionInfo* assertion,
						 bool succeeded)
{
	printf("Dump path: %ls\n", dump_path);
	return succeeded;
}

#else
#include <client/linux/handler/exception_handler.h>
static bool dumpCallback(const google_breakpad::MinidumpDescriptor& descriptor,
						 void* context, bool succeeded)
{
	printf("Dump path: %s\n", descriptor.path());
	return succeeded;
}
#endif
#endif


#define DISABLE_JLIB_LOG2
#include <ademco_packet.h>

#define ENABLE_COMMON_MACHINE_STATUS_TO_STRING
#define ENABLE_COMMON_MACHINE_TYPE_TO_STRING
#define ENABLE_COMMON_ZONE_PROPERTY_TO_STRING
#include <hb_detail.h>
#include <hb_helper.h>

using namespace ademco;
using namespace hb;
using namespace hb::common;

DECLARE_HB_COMMON_COM_CONSTEXPR_MEMBERS;


enum class QueryStage {
	None,

	WaitingSettingsMode,
	QueryingZones,
	QueryingLostConfig,

	WaitingSettingsMode2,
	QueryingTimer,
};

struct ZonePropertyAndLostConfig {
	ZoneProperty prop = ZoneProperty::InvalidZoneProperty;
	bool tamper_enabled = false; // ʧ������
};

struct Client {
	int fd = 0;
	evbuffer* output = nullptr;
	std::string acct = {};
	size_t ademco_id = 0;
	uint16_t seq = 0;
	int type = -1;
	MachineStatus status = MachineStatus::Arm;
	MachineStatus status1 = MachineStatus::Arm;
	MachineStatus status2 = MachineStatus::Arm;
	MachineStatus status3 = MachineStatus::Arm;
	std::map<size_t, ZonePropertyAndLostConfig> zones = {};
	QueryStage queryStage = QueryStage::None;
	com::MachineTimer timer = {};

	uint16_t nextSeq() { if (++seq == 10000) { seq = 1; } return seq; }

	void printInfo() const;
	Client& operator=(const Client&) = default;
};

struct ThreadContext {
	int worker_id = -1;
	event_base* base = nullptr;
	std::mutex mutex = {};
	std::unordered_map<int, Client> clients = {};
	AdemcoPacket packet = {};
};

event_base* listen_thread_evbase = nullptr;
int thread_count = 4;
std::vector<ThreadContext*> worker_thread_contexts = {};

// Commands
// M
#define COMMAND_M_EGZ EVENT_INVALID_EVENT
// C
#define COMMAND_C_AEGZ ((ADEMCO_EVENT)(EVENT_INVALID_EVENT + 1))
// X
#define COMMAND_X_AEGZX ((ADEMCO_EVENT)(EVENT_INVALID_EVENT + 2))
// Y
#define COMMAND_Y_AEGZX ((ADEMCO_EVENT)(EVENT_INVALID_EVENT + 3))
// Z
#define COMMAND_Z_QUERY_ZONE ((ADEMCO_EVENT)(EVENT_INVALID_EVENT + 4))
// F 
#define COMMAND_F_QUERY_TIMER ((ADEMCO_EVENT)(EVENT_INVALID_EVENT + 5))


// events will be sent to all clients
std::vector<ADEMCO_EVENT> commands = {};
int threads_to_handled_event = 0;
std::mutex mutex = {};
struct UserInput {
	char pwd[1024] = { 0 };
	int ademco_id = 0;
	int ev = 0;
	int gg = 0;
	int zone = 0;
	char xdata[1024] = { 0 };
}userInput = {};


int disable_data_print = 0;

void op_usage()
{
	printf("Press a key and hit <enter>:\n"
		   "A: Arm\n"
		   "D: Disarm, require 6 digits password\n"
		   "E: Emergency\n"
		   "T: Query machine type\n"
		   "M: Mannualy input [event gg zone], Exampel Input: 'M' <enter> 3400 1 0 <enter>\n"
		   "C: Like M, not send to all clients, but send to specific client with ademco_id: [ademco_id event gg zone]\n"
		   "X: Like C, with xdata: [ademco_id event gg zone xdata]\n"
		   "Y: Like X, with xdata, but xdata is hex: [ademco_id event gg zone xdata], example: [1 1704 0 0 EBAB3FA176]\n"
		   "Z: Query Zone info: [ademco_id]\n"
		   "F: Query Machine Timer: [ademco_id]\n"
		   "\n"
		   "I: Print clients info\n"
		   "P: Toggle enable/disable data print\n"
		   "Q: Quit\n"
		   );
}

void usage(const char* name)
{
	printf("Usage: %s [listening_port] [thread_count] [disable_data_print]\n", name);
}

void handle_com_passthrough(ThreadContext* context, Client& client, evbuffer* output)
{	
	if (!context->packet.xdata_) {
		printf("\tWARNING! 1704 NO XDATA!\n");
		return;
	}

	// GRPS cannot send us hex data, we need to convert data like "1234ABCDEF" to hex 0x1234ABCDEF 
	std::vector<char> xdata(context->packet.xdata_->data_.size() / 2);
	detail::ConvertHiLoAsciiToHexCharArray(&xdata[0], context->packet.xdata_->data_.data(), context->packet.xdata_->data_.size());
	printf("\t%s\n", detail::toString(xdata, detail::ToStringOption::ALL_CHAR_AS_HEX, false, false).data());

	char buf[1024];
	auto resp_type = com::ResponseParser::parse((const Char*)xdata.data(), xdata.size() & 0xFF);
	switch (resp_type) {
	case com::ResponseParser::ResponseType::A0_response:
		break;
	case com::ResponseParser::ResponseType::A2_response:
		{
			com::Resp_A2 resp; memcpy(resp.data, xdata.data(), xdata.size()); resp.len = xdata.size() & 0xFF;
			com::Resp_A2::ZoneAndProperties zps; bool hasMore = false;
			if (client.queryStage == QueryStage::QueryingZones && resp.parse(zps, hasMore)) {
				for (const auto& zp : zps) {
					ZonePropertyAndLostConfig zplc;
					zplc.prop = zp.prop;
					zplc.tamper_enabled = false;
					client.zones[zp.zone] = zplc;
					snprintf(buf, sizeof(buf), getZoneFormatString(machineTypeFromAdemcoEvent((ADEMCO_EVENT)client.type)), zp.zone);
					printf("\t\tZone:");
					printf("%s", buf);
					printf("  Prop:0x%02X %s\n", zp.prop, zonePropertyToStringEn(zp.prop));
				}
				XDataPtr xdata;
				if (hasMore) { // ������Ҫʣ�����
					com::Req_A2 req;
					xdata = makeXData((const char*)req.data, req.len);					
				} else { // ��ʼ��Ҫ����ʧ������
					com::Req_AC req;
					xdata = makeXData((const char*)req.data, req.len);
					client.queryStage = QueryStage::QueryingLostConfig;
				}

				auto n = context->packet.make_hb(buf, sizeof(buf), client.nextSeq(), client.acct, client.ademco_id, 0, 
												 EVENT_COM_PASSTHROUGH, 0, xdata);
				evbuffer_add(output, buf, n);
				if (!disable_data_print) {
					printf("T#%d S#%d acct=%s ademco_id=%06zX :%s\n",
						   context->worker_id, client.fd, client.acct.data(), client.ademco_id,
						   context->packet.toString(detail::ToStringOption::ALL_CHAR_AS_HEX).data());
				}
			} else {
				auto n = context->packet.make_hb(buf, sizeof(buf), client.nextSeq(), client.acct, client.ademco_id, 0,
												 EVENT_EXIT_SET_MODE, 0);
				evbuffer_add(output, buf, n);
				if (!disable_data_print) {
					printf("T#%d S#%d acct=%s ademco_id=%06zX :%s\n",
						   context->worker_id, client.fd, client.acct.data(), client.ademco_id,
						   context->packet.toString(detail::ToStringOption::ALL_CHAR_AS_HEX).data());
				}
			}
			break;
		}
	case com::ResponseParser::ResponseType::A3_response:
		break;
	case com::ResponseParser::ResponseType::A4_response:
		break;
	case com::ResponseParser::ResponseType::A6_response:
		{
			if (client.queryStage == QueryStage::QueryingTimer) {
				client.queryStage = QueryStage::None;
				com::Resp_A6 resp; memcpy(resp.data, xdata.data(), resp.len);
				client.timer = resp.getTimer();
				for (int i = 0; i < 2; i++) {
					if (com::isValidMachineTimer(client.timer.timer[i])) {
						printf("\t\tMachine Timer#%d: Arm at %02d:%02d, Disarm at %02d:%02d\n", i + 1,
							   client.timer.timer[i].armAt.hour, client.timer.timer[i].armAt.minute,
							   client.timer.timer[i].disarmAt.hour, client.timer.timer[i].disarmAt.minute);
					} else {
						printf("\t\tMachine Timer#%d: Not Set\n", i + 1);
					}
				}
			}
			auto n = context->packet.make_hb(buf, sizeof(buf), client.nextSeq(), client.acct, client.ademco_id, 0,
											 EVENT_EXIT_SET_MODE, 0);
			evbuffer_add(output, buf, n);
			if (!disable_data_print) {
				printf("T#%d S#%d acct=%s ademco_id=%06zX :%s\n",
					   context->worker_id, client.fd, client.acct.data(), client.ademco_id,
					   context->packet.toString(detail::ToStringOption::ALL_CHAR_AS_HEX).data());
			}
		}
		break;
	case com::ResponseParser::ResponseType::A7_response:
		break;
	case com::ResponseParser::ResponseType::A8_response:
		break;
	case com::ResponseParser::ResponseType::A9_response:
		break;
	case com::ResponseParser::ResponseType::AB_response:
		break;
	case com::ResponseParser::ResponseType::AD_response:
		{
			com::Resp_AD resp; memcpy(resp.data, xdata.data(), xdata.size()); resp.len = xdata.size() & 0xFF;
			bool hasMore = false;
			std::vector<size_t> zones;
			if (client.queryStage == QueryStage::QueryingLostConfig && resp.parse(zones, hasMore)) {
				for (const auto& zone : zones) {
					client.zones[zone].tamper_enabled = true;
					snprintf(buf, sizeof(buf), getZoneFormatString(machineTypeFromAdemcoEvent((ADEMCO_EVENT)client.type)), zone);
					printf("\t\tZone:");
					printf("%s", buf);
					printf("  Tamper Enabled: true\n");
				}
				XDataPtr xdata;
				if (hasMore) { // ������Ҫʣ�����
					com::Req_A2 req;
					xdata = makeXData((const char*)req.data, req.len);
					auto n = context->packet.make_hb(buf, sizeof(buf), client.nextSeq(), client.acct, client.ademco_id, 0,
													 EVENT_COM_PASSTHROUGH, 0, xdata);
					evbuffer_add(output, buf, n);
					if (!disable_data_print) {
						printf("T#%d S#%d acct=%s ademco_id=%06zX :%s\n",
							   context->worker_id, client.fd, client.acct.data(), client.ademco_id,
							   context->packet.toString(detail::ToStringOption::ALL_CHAR_AS_HEX).data());
					}
					break;
				}
			} 
			auto n = context->packet.make_hb(buf, sizeof(buf), client.nextSeq(), client.acct, client.ademco_id, 0,
											 EVENT_EXIT_SET_MODE, 0);
			evbuffer_add(output, buf, n);
			if (!disable_data_print) {
				printf("T#%d S#%d acct=%s ademco_id=%06zX :%s\n",
					   context->worker_id, client.fd, client.acct.data(), client.ademco_id,
					   context->packet.toString(detail::ToStringOption::ALL_CHAR_AS_HEX).data());
			}
			break;
		}
	case com::ResponseParser::ResponseType::AE_response:
		break;
	case com::ResponseParser::ResponseType::B1_response:
		{			
			com::Resp_B1 b1; memcpy(b1.data.data, xdata.data(), xdata.size());
			client.status1 = b1.data.cmd.status1 == 0 ? MachineStatus::Arm : MachineStatus::Disarm;
			client.status2 = b1.data.cmd.status2 == 0 ? MachineStatus::Arm : MachineStatus::Disarm;
			client.status3 = b1.data.cmd.status3 == 0 ? MachineStatus::Arm : MachineStatus::Disarm;
			printf("\t\t status1: %d %s\n\t\t status2: %d %s\n\t\t status3: %d %s\n",
					(int)client.status1, machineStatusToString(client.status1),
					(int)client.status2, machineStatusToString(client.status2),
					(int)client.status3, machineStatusToString(client.status3));			
			
			break;
		}
	case com::ResponseParser::ResponseType::Invalid_response:
		break;
	default:
		break;
	}

}

void handle_ademco_msg(ThreadContext* context, bufferevent* bev)
{
	int fd = (int)bufferevent_getfd(bev);
	auto& client = context->clients[fd];
	if (!disable_data_print) {
		printf("T#%d C#%d acct=%s ademco_id=%06zX :%s\n",
			   context->worker_id, fd, client.acct.data(), client.ademco_id, context->packet.toString().data());
	}
	auto output = bufferevent_get_output(bev);
	switch (context->packet.id_.eid_) {
	case AdemcoId::id_null:
	case AdemcoId::id_hb:
	case AdemcoId::id_admcid:
		{
			char buf[1024];
			if (context->packet.id_.eid_ != AdemcoId::id_null) {
				client.acct = context->packet.acct_.acct();
				client.ademco_id = context->packet.ademcoData_.ademco_id_;
				if (ademco::isMachineTypeEvent(context->packet.ademcoData_.ademco_event_)) {
					client.type = context->packet.ademcoData_.ademco_event_;
					if (client.type == EVENT_I_AM_3_SECTION_MACHINE) { // ������������Ҫ������Ҫ����״̬
						com::Req_B0 req;
						auto xdata = makeXData((const char*)req.data, req.len);
						auto n = context->packet.make_hb(buf, sizeof(buf), client.nextSeq(), client.acct, client.ademco_id, 0, 
														 EVENT_COM_PASSTHROUGH, 0, xdata);
						evbuffer_add(output, buf, n);
						if (!disable_data_print) {
							printf("T#%d S#%d acct=%s ademco_id=%06zX :%s\n",
								   context->worker_id, fd, client.acct.data(), client.ademco_id, 
								   context->packet.toString(detail::ToStringOption::ALL_CHAR_AS_HEX).data());
						}
						break;
					}
				} else if (ademco::isMachineStatusEvent(context->packet.ademcoData_.ademco_event_)) {
					auto status = machienStatusFromAdemcoEvent(context->packet.ademcoData_.ademco_event_);
					if (context->packet.ademcoData_.gg_ == 0) { client.status = status; }
					else if (context->packet.ademcoData_.gg_ == 1) { client.status1 = status; } 
					else if (context->packet.ademcoData_.gg_ == 2) { client.status2 = status; } 
					else if (context->packet.ademcoData_.gg_ == 3) { client.status3 = status; }
					else { break; }
				} else if (context->packet.ademcoData_.ademco_event_ == EVENT_COM_PASSTHROUGH) {
					handle_com_passthrough(context, client, output);
					break;
				} else if (context->packet.ademcoData_.ademco_event_ == EVENT_ENTER_SET_MODE) {
					if (client.queryStage == QueryStage::WaitingSettingsMode) {
						client.queryStage = QueryStage::QueryingZones;
						com::Req_A1 req;
						auto xdata = makeXData((const char*)req.data, req.len);
						auto n = context->packet.make_hb(buf, sizeof(buf), client.nextSeq(), client.acct, client.ademco_id, 0,
														 EVENT_COM_PASSTHROUGH, 0, xdata);
						evbuffer_add(output, buf, n);
						if (!disable_data_print) {
							printf("T#%d S#%d acct=%s ademco_id=%06zX :%s\n",
								   context->worker_id, fd, client.acct.data(), client.ademco_id,
								   context->packet.toString(detail::ToStringOption::ALL_CHAR_AS_HEX).data());
						}
					} else if (client.queryStage == QueryStage::WaitingSettingsMode2) {
						client.queryStage = QueryStage::QueryingTimer;
						com::Req_A5 req;
						auto xdata = makeXData((const char*)req.data, req.len);
						auto n = context->packet.make_hb(buf, sizeof(buf), client.nextSeq(), client.acct, client.ademco_id, 0,
														 EVENT_COM_PASSTHROUGH, 0, xdata);
						evbuffer_add(output, buf, n);
						if (!disable_data_print) {
							printf("T#%d S#%d acct=%s ademco_id=%06zX :%s\n",
								   context->worker_id, fd, client.acct.data(), client.ademco_id,
								   context->packet.toString(detail::ToStringOption::ALL_CHAR_AS_HEX).data());
						}
					}
					
				}
			}

			// ACK
			{
				size_t n = context->packet.make_ack(buf, sizeof(buf), context->packet.seq_.value_, 
													context->packet.acct_.acct(), context->packet.ademcoData_.ademco_id_);
				evbuffer_add(output, buf, n);
				if (!disable_data_print) {
					printf("T#%d S#%d acct=%s ademco_id=%06zX :%s\n",
						   context->worker_id, fd, client.acct.data(), client.ademco_id, context->packet.toString().data());
				}
			}
			break;
		}
	default:
		break;
	}
}

// send all buffered events to all clients
void commandcb(evutil_socket_t, short, void* user_data)
{
	auto context = (ThreadContext*)user_data;
	std::vector<ADEMCO_EVENT> evs;

	{
		std::lock_guard<std::mutex> lg(mutex);
		if (--threads_to_handled_event == 0) {
			evs = std::move(commands);
		} else {
			evs = commands;
		}
	}

	char buf[1024];

	std::lock_guard<std::mutex> lg(context->mutex);
	for (auto& client : context->clients) {
		for (auto e : evs) {			
			size_t n = 0;
			if (e == COMMAND_M_EGZ) { // M
				n = context->packet.make_hb(buf, sizeof(buf), client.second.nextSeq(), client.second.acct, client.second.ademco_id,
											userInput.gg, (ADEMCO_EVENT)userInput.ev, userInput.zone);
				evbuffer_add(client.second.output, buf, n);
				if (!disable_data_print) {
					printf("T#%d S#%d acct=%s ademco_id=%06zX :%s\n",
						   context->worker_id, client.second.fd, client.second.acct.data(), client.second.ademco_id, 
						   context->packet.toString().data());
				}
			} else if (e == (COMMAND_C_AEGZ)) { // C
				if (client.second.ademco_id != userInput.ademco_id) {
					continue;
				}
				n = context->packet.make_hb(buf, sizeof(buf), client.second.nextSeq(), client.second.acct, client.second.ademco_id,
											userInput.gg, (ADEMCO_EVENT)userInput.ev, userInput.zone);
				evbuffer_add(client.second.output, buf, n);
				if (!disable_data_print) {
					printf("T#%d S#%d acct=%s ademco_id=%06zX :%s\n",
						   context->worker_id, client.second.fd, client.second.acct.data(), client.second.ademco_id, 
						   context->packet.toString().data());
				}
			} else if (e == (COMMAND_X_AEGZX)) { // X
				if (client.second.ademco_id != userInput.ademco_id) {
					continue;
				}
				auto xdata = makeXData(userInput.xdata, strlen(userInput.xdata));
				n = context->packet.make_hb(buf, sizeof(buf), client.second.nextSeq(), client.second.acct, client.second.ademco_id,
											userInput.gg, (ADEMCO_EVENT)userInput.ev, userInput.zone, xdata);
				evbuffer_add(client.second.output, buf, n);
				if (!disable_data_print) {
					printf("T#%d S#%d acct=%s ademco_id=%06zX :%s\n",
						   context->worker_id, client.second.fd, client.second.acct.data(), client.second.ademco_id, 
						   context->packet.toString().data());
				}
			} else if (e == (COMMAND_Y_AEGZX)) { // Y
				if (client.second.ademco_id != userInput.ademco_id) {
					continue;
				}
				std::vector<char> data(strlen(userInput.xdata) / 2);
				ademco::detail::ConvertHiLoAsciiToHexCharArray(&data[0], userInput.xdata, strlen(userInput.xdata));
				auto xdata = makeXData(data);
				n = context->packet.make_hb(buf, sizeof(buf), client.second.nextSeq(), client.second.acct, client.second.ademco_id,
											userInput.gg, (ADEMCO_EVENT)userInput.ev, userInput.zone, xdata);
				evbuffer_add(client.second.output, buf, n);
				if (!disable_data_print) {
					printf("T#%d S#%d acct=%s ademco_id=%06zX :%s\n",
						   context->worker_id, client.second.fd, client.second.acct.data(), client.second.ademco_id, 
						   context->packet.toString(ademco::detail::ToStringOption::ALL_CHAR_AS_HEX).data());
				}
			} else if (e == COMMAND_Z_QUERY_ZONE) {
				if (client.second.ademco_id != userInput.ademco_id) {
					continue;
				}
				n = context->packet.make_hb(buf, sizeof(buf), client.second.nextSeq(), client.second.acct, client.second.ademco_id, 0, 
											EVENT_ENTER_SET_MODE, 0);
				evbuffer_add(client.second.output, buf, n);
				client.second.zones.clear();
				client.second.queryStage = QueryStage::WaitingSettingsMode;
				if (!disable_data_print) {
					printf("T#%d S#%d acct=%s ademco_id=%06zX :%s\n",
						   context->worker_id, client.second.fd, client.second.acct.data(), client.second.ademco_id,
						   context->packet.toString(ademco::detail::ToStringOption::ALL_CHAR_AS_HEX).data());
				}

			} else if (e == COMMAND_F_QUERY_TIMER) { // F
				if (client.second.ademco_id != userInput.ademco_id) {
					continue;
				}
				n = context->packet.make_hb(buf, sizeof(buf), client.second.nextSeq(), client.second.acct, client.second.ademco_id, 0,
											EVENT_ENTER_SET_MODE, 0);
				evbuffer_add(client.second.output, buf, n);
				client.second.zones.clear();
				client.second.queryStage = QueryStage::WaitingSettingsMode2;
				if (!disable_data_print) {
					printf("T#%d S#%d acct=%s ademco_id=%06zX :%s\n",
						   context->worker_id, client.second.fd, client.second.acct.data(), client.second.ademco_id,
						   context->packet.toString(ademco::detail::ToStringOption::ALL_CHAR_AS_HEX).data());
				}
			} else if (client.second.type == EVENT_I_AM_3_SECTION_MACHINE && (e == EVENT_ARM || e == EVENT_DISARM)) {
				for (int gg = 1; gg <= 3; gg++) {
					if (e == EVENT_DISARM) {
						auto xdata = makeXData(userInput.pwd, 6);
						n = context->packet.make_hb(buf, sizeof(buf), client.second.nextSeq(), client.second.acct, client.second.ademco_id, gg, e, 0, xdata);
					} else {
						n = context->packet.make_hb(buf, sizeof(buf), client.second.nextSeq(), client.second.acct, client.second.ademco_id, gg, e, 0);
					}
					evbuffer_add(client.second.output, buf, n);
					if (!disable_data_print) {
						printf("T#%d S#%d acct=%s ademco_id=%06zX :%s\n",
							   context->worker_id, client.second.fd, client.second.acct.data(), client.second.ademco_id, 
							   context->packet.toString().data());
					}
				}
			} else {
				if (e == EVENT_DISARM) {
					auto xdata = makeXData(userInput.pwd, 6);
					n = context->packet.make_hb(buf, sizeof(buf), client.second.nextSeq(), client.second.acct, client.second.ademco_id, 0, e, 0, xdata);
				} else {
					n = context->packet.make_hb(buf, sizeof(buf), client.second.nextSeq(), client.second.acct, client.second.ademco_id, 0, e, 0);
				}
				evbuffer_add(client.second.output, buf, n);
				if (!disable_data_print) {
					printf("T#%d S#%d acct=%s ademco_id=%06zX :%s\n",
						   context->worker_id, client.second.fd, client.second.acct.data(), client.second.ademco_id, 
						   context->packet.toString().data());
				}
			}
		}
	}
}

void readcb(struct bufferevent* bev, void* user_data)
{
	auto input = bufferevent_get_input(bev);
	auto context = (ThreadContext*)user_data;
	std::lock_guard<std::mutex> lg(context->mutex);

	while (1) {
		size_t len = evbuffer_get_length(input);
		if (len < 9) { return; }
		char buff[1024] = { 0 };
		evbuffer_copyout(input, buff, std::min(len, sizeof(buff)));
		size_t cb_commited = 0;
		auto res = context->packet.parse(buff, 1024, cb_commited);
		bool done = false;
		switch (res) {
		case ademco::ParseResult::RESULT_OK:
			evbuffer_drain(input, cb_commited);
			handle_ademco_msg(context, bev);
			break;
		case ademco::ParseResult::RESULT_NOT_ENOUGH:
			done = true;
			break;
		case ademco::ParseResult::RESULT_DATA_ERROR:
			fprintf(stderr, "error while parsing data\n");
			evbuffer_drain(input, len);
			done = true;
			break;
		}

		if (done) { break; }
	}
}

void eventcb(struct bufferevent* bev, short events, void* user_data)
{
	int fd = (int)bufferevent_getfd(bev);
	printf("eventcb events=%04X\n", events);
	if (events & BEV_EVENT_EOF) {
	} else if (events & (BEV_EVENT_WRITING)) {
		printf("Got an error while writing #%d.\n", fd);
	} else if (events & (BEV_EVENT_ERROR)) {
		printf("Got an error on the connection %d: %s\n",
			   fd, strerror(errno));
	}
	printf("Connection #%d closed.\n", fd); 
	auto context = (ThreadContext*)user_data;
	{
		std::lock_guard<std::mutex> lg(context->mutex);
		context->clients.erase(fd);
	}
	bufferevent_free(bev);
}

void accept_cb(evconnlistener* listener, evutil_socket_t fd, sockaddr* addr, int socklen, void* user_data)
{
	char str[INET_ADDRSTRLEN] = { 0 };
	auto sin = (sockaddr_in*)addr;
	inet_ntop(AF_INET, &sin->sin_addr, str, INET_ADDRSTRLEN);
	printf("accpet TCP connection #%d from: %s:%d\n", (int)fd, str, sin->sin_port);
	//evutil_make_socket_nonblocking(fd);

	static int worker_id = 0;
	auto context = worker_thread_contexts[worker_id];
	std::lock_guard<std::mutex> lg(context->mutex);
	auto bev = bufferevent_socket_new(context->base, fd, BEV_OPT_CLOSE_ON_FREE);
	if (!bev) {
		fprintf(stderr, "Error constructing bufferevent!\n");
		event_base_loopbreak(context->base);
		return;
	}

	Client client;
	client.fd = (int)fd;
	client.output = bufferevent_get_output(bev);
	context->clients[(int)fd] = client;

	bufferevent_setcb(bev, readcb, nullptr, eventcb, context);
	bufferevent_enable(bev, EV_WRITE | EV_READ);
	worker_id = (++worker_id) % thread_count;	
}

void accpet_error_cb(evconnlistener* listener, void* context)
{
	auto base = evconnlistener_get_base(listener);
	int err = EVUTIL_SOCKET_ERROR();
	fprintf(stderr, "accpet_error_cb:%d:%s\n", err, evutil_socket_error_to_string(err));
	event_base_loopexit(base, nullptr);
}

void init_listener_thread(const sockaddr_in& addr)
{
	listen_thread_evbase = event_base_new();
	if (!listen_thread_evbase) {
		fprintf(stderr, "init libevent failed\n");
		exit(-1);
	}

	auto listener = evconnlistener_new_bind(listen_thread_evbase,
											accept_cb,
											nullptr,
											LEV_OPT_REUSEABLE | LEV_OPT_CLOSE_ON_FREE,
											-1, // backlog, -1 for default
											(const sockaddr*)(&addr),
											sizeof(addr));
	if (!listener) {
		fprintf(stderr, "create listener failed\n");
		exit(-1);
	}
	evconnlistener_set_error_cb(listener, accpet_error_cb);
}

void dummy_cb_avoid_worker_exit(evutil_socket_t, short, void*)
{
}

ThreadContext* init_worker_thread(int i)
{
	ThreadContext* context = new ThreadContext();
	context->worker_id = i;
	context->base = event_base_new();
	if (!context->base) {
		fprintf(stderr, "init libevent failed\n");
		exit(-1);
	}
	timeval tv = { 1, 0 };
	event_add(event_new(context->base, -1, EV_PERSIST, dummy_cb_avoid_worker_exit, nullptr), &tv);
	printf("worker thread #%d created\n", i);
	return context;
}

void clear_stdin()
{
	int ret = scanf("%*[^\n]%*c");
	(void)ret;
}

int main(int argc, char** argv)
{
	usage(argv[0]);

#ifdef _WIN32
	WSADATA wsa_data;
	WSAStartup(0x0201, &wsa_data);
	if (0 != evthread_use_windows_threads()) {
		fprintf(stderr, "failed to init libevent with thread by calling evthread_use_windows_threads\n");
		return -1;
	}
#if ENABLE_BREAKPAD 
	google_breakpad::ExceptionHandler eh(L"./", // dump_path
										 nullptr, // FilterCallback 
										 dumpCallback, // MinidumpCallback 
										 nullptr, // callback_context
										 google_breakpad::ExceptionHandler::HANDLER_ALL // handler_types
	); // MINIDUMP_TYPE
#endif
#else 
	if (0 != evthread_use_pthreads()) {
		fprintf(stderr, "failed to init libevent with thread by calling evthread_use_pthreads\n");
		return -1;
	}
#if ENABLE_BREAKPAD 
	google_breakpad::ExceptionHandler eh(google_breakpad::MinidumpDescriptor("./"), nullptr, dumpCallback, nullptr, true, -1);
#endif
#endif

	int port = 12345; 
	if (argc > 1) {
		port = atoi(argv[1]);
		if (port <= 0 || port > 65535) {
			puts("Invalid port");
			return 1;
		}
	}

	if (argc > 2) {
		thread_count = atoi(argv[2]);
		if (thread_count <= 0) {
			puts("Invalid thread_count");
			return 1;
		}
	}

	if (argc > 3) {
		disable_data_print = atoi(argv[3]) == 1;
	}

	sockaddr_in sin = { 0 };
	sin.sin_family = AF_INET;
	sin.sin_addr.s_addr = htonl(INADDR_ANY);
	sin.sin_port = htons(port);

	printf("using libevent %s\n", event_get_version());
	printf("%s is listening on port %d using %d threads, disable_data_print=%d\n",
		   argv[0], port, thread_count, disable_data_print);

	std::vector<std::thread> worker_threads;
	for (int i = 0; i < thread_count; i++) {
		auto context = init_worker_thread(i);
		worker_thread_contexts.push_back(context);
		worker_threads.emplace_back(std::thread([context]() {
			event_base_dispatch(context->base);
		}));
	}

	std::thread listener_thread([&sin]() {
		init_listener_thread(sin); 
		event_base_dispatch(listen_thread_evbase);
	});

	op_usage();

	auto fire_command = []() {
		struct timeval tv = { 0, 1000 };
		for (int i = 0; i < thread_count; i++) {
			event_add(event_new(worker_thread_contexts[i]->base, -1, 0, commandcb, worker_thread_contexts[i]), &tv);
		}
	};

	bool running = true;
	while (running) {
		int cmd = getchar(); 
		if ('a' <= cmd && cmd <= 'z') {
			cmd -= 32;
		}

		switch (cmd) {
		case 'A':
			{
				std::lock_guard<std::mutex> lg(mutex);
				commands.push_back(EVENT_ARM);
				threads_to_handled_event = thread_count;
			}
			fire_command();
			break;

		case 'D':
			{
				int ret = 0;
				do {
					printf("Input 6 digit password:");
					ret = scanf("%s", userInput.pwd);
					clear_stdin();
				} while (ret != 1 || strlen(userInput.pwd) != 6);
				{
					std::lock_guard<std::mutex> lg(mutex);
					commands.push_back(EVENT_DISARM);
					threads_to_handled_event = thread_count;
				}
				fire_command();
			break;
			}

		case 'E':
			{
				std::lock_guard<std::mutex> lg(mutex);
				commands.push_back(EVENT_EMERGENCY);
				threads_to_handled_event = thread_count;
			}
			fire_command();
			break;

		case 'T':
			{
				std::lock_guard<std::mutex> lg(mutex);
				commands.push_back(EVENT_WHAT_IS_YOUR_TYPE);
				threads_to_handled_event = thread_count;
			}
			fire_command();
			break;

		case 'M':
			{
				int ret = 0;
				do {
					printf("Input [event gg zone]:");
					ret = scanf("%d %d %d", &userInput.ev, &userInput.gg, &userInput.zone);
					clear_stdin();
				} while (ret != 3);
				{
					std::lock_guard<std::mutex> lg(mutex);
					commands.push_back(COMMAND_M_EGZ);
					threads_to_handled_event = thread_count;
				}
				fire_command();
				break;
			}

		case 'C':
			{
				int ret = 0;
				do {
					printf("Input [ademco_id event gg zone]:");
					ret = scanf("%d %d %d %d", &userInput.ademco_id, &userInput.ev, &userInput.gg, &userInput.zone);
					clear_stdin();
				} while (ret != 4);
				{
					std::lock_guard<std::mutex> lg(mutex);
					commands.push_back(COMMAND_C_AEGZ);
					threads_to_handled_event = thread_count;
				}
				fire_command();
				break;
			}

		case 'X':
		case 'Y':
			{
				int ret = 0;
				do {
					printf("Input [ademco_id event gg zone xdata]:");
					ret = scanf("%d %d %d %d %s", &userInput.ademco_id, &userInput.ev, &userInput.gg, &userInput.zone, userInput.xdata);
					clear_stdin();
				} while (ret != 5);
				{
					std::lock_guard<std::mutex> lg(mutex);
					commands.push_back(cmd == 'X' ? COMMAND_X_AEGZX : COMMAND_Y_AEGZX);
					threads_to_handled_event = thread_count;
				}
				fire_command();
				break;
			}

		case 'Z':
			{
				int ret = 0;
				do {
					printf("Input [ademco_id]:");
					ret = scanf("%d", &userInput.ademco_id);
					clear_stdin();
				} while (ret != 1);
				{
					std::lock_guard<std::mutex> lg(mutex);
					commands.push_back(COMMAND_Z_QUERY_ZONE);
					threads_to_handled_event = thread_count;
				}
				fire_command();
				break;
			}

		case 'F':
			{
				int ret = 0;
				do {
					printf("Input [ademco_id]:");
					ret = scanf("%d", &userInput.ademco_id);
					clear_stdin();
				} while (ret != 1);
				{
					std::lock_guard<std::mutex> lg(mutex);
					commands.push_back(COMMAND_F_QUERY_TIMER);
					threads_to_handled_event = thread_count;
				}
				fire_command();
				break;
			}


		case 'I':
			{
				std::vector<Client> copiedClients;
				for (auto context : worker_thread_contexts) {
					std::lock_guard<std::mutex> lg(context->mutex);
					for (const auto& client : context->clients) {
						copiedClients.push_back(client.second);
					}
				}
				printf("Total connnected %zd clients:\n", copiedClients.size());

				for (const auto& client : copiedClients) {
					client.printInfo();
				}
			}
			break;

		case 'P':
			disable_data_print = !disable_data_print;
			printf("Data print is %s\n", disable_data_print ? "Off" : "On");
			break;

		case 'Q':
			{
				timeval tv{ 0, 1000 };
				event_base_loopexit(listen_thread_evbase, &tv);
				listener_thread.join();
				printf("listener thread exited\n");
				for (int i = 0; i < thread_count; i++) {
					event_base_loopexit(worker_thread_contexts[i]->base, &tv);
					worker_threads[i].join();
					printf("worker_thread #%d exited\n", i);
				}
				running = false;
				break;
			}
			
		case '\r':
		case '\n':
			break;

		default:
			clear_stdin();
			printf("Invalid command\n");
			op_usage(); 
			break;
		}		
	}

	printf("Bye\n");

	return 0;
}

void Client::printInfo() const
{
	if (type == EVENT_I_AM_3_SECTION_MACHINE) {
		printf("  fd=#%d acct=%s ademco_id=%zd type=%s\n",
			   fd, acct.data(), ademco_id,
			   machineTypeToString(machineTypeFromAdemcoEvent((ADEMCO_EVENT)type)));
		printf("    status1: %d %s    status2: %d %s    status3: %d %s\n",
			   (int)status1, machineStatusToString(status1),
			   (int)status2, machineStatusToString(status2),
			   (int)status3, machineStatusToString(status3));
	} else {
		printf("  fd=#%d acct=%s ademco_id=%zd type=%s status=%d %s\n",
			   fd, acct.data(), ademco_id,
			   machineTypeToString(machineTypeFromAdemcoEvent((ADEMCO_EVENT)type)),
			   (int)status, machineStatusToString(status));
	}
	for (const auto& zp : zones) {
		char buf[512];
		snprintf(buf, sizeof(buf), getZoneFormatString(machineTypeFromAdemcoEvent((ADEMCO_EVENT)type)), zp.first);
		printf("    Zone:");
		printf("%s", buf);
		printf("  Prop:0x%02X %s     \tTamper Enabled:%s\n",
			   zp.second.prop, zonePropertyToStringEn(zp.second.prop),
			   zp.second.tamper_enabled ? "true" : "false");
	}
	for (int i = 0; i < 2; i++) {
		if (com::isValidMachineTimer(timer.timer[i])) {
			printf("    Machine Timer#%d: Arm at %02d:%02d, Disarm at %02d:%02d\n", i + 1,
				   timer.timer[i].armAt.hour, timer.timer[i].armAt.minute,
				   timer.timer[i].disarmAt.hour, timer.timer[i].disarmAt.minute);
		} else {
			printf("    Machine Timer#%d: Not Set\n", i + 1);
		}
	}
}