#include <ctime>
#include <sys/time.h>

#include "n2nvpn.h"

extern "C" {
	#include "random_numbers.h"
	#include "sn_selection.h"
	#include "pearson.h"
	#include "uthash.h"
	#ifdef _WIN32
		#include "win32/defs.h"
	#else
		#include <arpa/inet.h>     // for inet_ntoa
		#include <netinet/in.h>    // for in_addr, htonl, in_addr_t
	#endif

	void    send_register_super(n2n_edge_t *eee);
	void    send_query_peer(n2n_edge_t *eee, const n2n_mac_t dst_mac);
	int     supernode_connect(n2n_edge_t *eee);
	int     supernode_disconnect(n2n_edge_t *eee);
	int     fetch_and_eventually_process_data(n2n_edge_t *eee, SOCKET sock, uint8_t *pktbuf, uint16_t *expected, uint16_t *position, time_t now);
	uint8_t resolve_check(n2n_resolve_parameter_t *param, uint8_t resolution_request, time_t now);
}


N2NVPN::N2NVPN() {
	n2n_srand(n2n_seed());
}


void N2NVPN::_bind_methods() {
	ClassDB::bind_method(D_METHOD("get_ip"), &N2NVPN::get_ip);
	ClassDB::bind_method(D_METHOD("get_mac"), &N2NVPN::get_mac);
	ClassDB::bind_method(D_METHOD("get_peers"), &N2NVPN::get_peers);
	ClassDB::bind_method(D_METHOD("reset_configuration"), &N2NVPN::reset_configuration);
	ClassDB::bind_method(D_METHOD("set_addresses", "ip_address", "mac_address"), &N2NVPN::set_addresses);
	ClassDB::bind_method(D_METHOD("set_binding", "ip_address", "port"), &N2NVPN::set_binding);
	ClassDB::bind_method(D_METHOD("set_compression", "enabled"), &N2NVPN::set_compression);
	ClassDB::bind_method(D_METHOD("set_management", "password", "port"), &N2NVPN::set_management);
	ClassDB::bind_method(D_METHOD("set_network", "user_name", "network_name", "network_password"), &N2NVPN::set_network);
	ClassDB::bind_method(D_METHOD("set_registration", "interval", "ttl"), &N2NVPN::set_registration);
	ClassDB::bind_method(D_METHOD("set_routing", "enabled"), &N2NVPN::set_routing);
	ClassDB::bind_method(D_METHOD("set_supernode", "address", "port"), &N2NVPN::set_supernode);
	ClassDB::bind_method(D_METHOD("start_network"), &N2NVPN::start_network);
	ClassDB::bind_method(D_METHOD("stop_network"), &N2NVPN::stop_network);
}


void N2NVPN::_edge_thread_function() {
	print_line("Entering run_edge_loop()");
	run_edge_loop(_eee);  // Blocks until _keep_running == false
	print_line("Exited run_edge_loop()");
	edge_term_conf(&_eee->conf);
	print_line("Closing tuntap");
	tuntap_close(&_eee->device);
	print_line("Terminating edge");
	edge_term(_eee);
	print_line("Exiting thread");
}


String N2NVPN::get_ip() {
	String result(_eee->tuntap_priv_conf.ip_addr);
	return result;
}


String N2NVPN::get_mac() {
	String result(_eee->tuntap_priv_conf.device_mac);
	return result;
}


Array N2NVPN::get_peers() {
	in_addr_t    net;
	peer_info_t *peer;
	peer_info_t *tempPeer;
	Array        results;
	int          x;

	HASH_ITER(hh, _eee->pending_peers, peer, tempPeer) {
		if (peer->dev_addr.net_addr != 0) {
			net = htonl(peer->dev_addr.net_addr);
			Dictionary entry;
			entry[Variant("type")] = Variant("pending");
			entry[Variant("ip")]   = Variant(inet_ntoa(*(struct in_addr *)&net));
			results.append(entry);
		}
	}
	HASH_ITER(hh, _eee->known_peers, peer, tempPeer) {
		if (peer->dev_addr.net_addr != 0) {
			net = htonl(peer->dev_addr.net_addr);
			Dictionary entry;
			entry[Variant("type")] = Variant("known");
			entry[Variant("ip")]   = Variant(inet_ntoa(*(struct in_addr *)&net));
			results.append(entry);
		}
	}

	return results;
}


void N2NVPN::reset_configuration() {
	edge_init_conf_defaults(&_conf);

	_conf.allow_p2p              = 1;                         // Whether to allow peer-to-peer communication
	_conf.allow_routing          = 1;                         // Whether to allow the edge to route packets to other edges
	_conf.disable_pmtu_discovery = 1;                         // Whether to disable the path MTU discovery
	_conf.drop_multicast         = 0;                         // Whether to disable multicast
	_conf.tuntap_ip_mode         = TUNTAP_IP_MODE_SN_ASSIGN;  // How to set the IP address
	_conf.local_port             = 0;                         // What port to use (0 = any port)
	_conf.mgmt_port              = N2N_EDGE_MGMT_PORT;        // Edge management port (5644 by default)
	_conf.register_interval      = 1;                         // Interval for both UDP NAT hole punching and supernode registration
	_conf.register_ttl           = 1;                         // Interval for UDP NAT hole punching through supernode
	_conf.tos                    = 16;                        // Type of service for sent packets
	_conf.transop_id             = N2N_TRANSFORM_ID_TWOFISH;  // Use the twofish encryption

	// _conf.network_traffic_filter_rules

	_conf.compression            = N2N_COMPRESSION_ID_ZSTD;

	_ip_address  = "";   // Empty addresses will cause them to be generated / acquired.
	_mac_address = "";
}


void N2NVPN::set_addresses(const String ip, const String mac) {
	_ip_address  = ip;
	_mac_address = mac;
}


void N2NVPN::set_binding(String ip, int16_t port) {
	if (ip != "") {
		_conf.bind_address = ntohl(inet_addr(ip.ascii().get_data()));
	} else {
		_conf.bind_address = INADDR_ANY;
	}
	_conf.local_port  = port;  // Local listen port, or 0 for any.
}


void N2NVPN::set_compression(bool enabled) {
	_conf.compression = (enabled ? N2N_COMPRESSION_ID_ZSTD : N2N_COMPRESSION_ID_NONE);
}


void N2NVPN::set_management(String password, int16_t port) {
	if (password != "") {
		_conf.mgmt_password_hash = pearson_hash_64((uint8_t *)password.ascii().get_data(), strlen(password.ascii().get_data()));
	}
	_conf.mgmt_port = port;
}


void N2NVPN::set_network(const String user_name, const String network_name, const String network_password) {
	_user_name        = user_name;
	_network_password = network_password;

	snprintf((char *)_conf.community_name, sizeof(_conf.community_name), "%s", network_name.ascii().get_data());  // Community to connect to
	_conf.encrypt_key = (char *)strdup(_network_password.ascii().get_data());  // Secret to decrypt & encrypt with.
}


void N2NVPN::set_registration(int16_t interval, int16_t ttl) {
	_conf.register_interval = interval;
	_conf.register_ttl      = ttl;
}


void N2NVPN::set_routing(bool enabled) {
	_conf.allow_routing = enabled ? 1 : 0;
}


void N2NVPN::set_supernode(const String address, int port) {
	String supernode = address + ":" + String::num_int64((int64_t)port);

	edge_conf_add_supernode(&_conf, supernode.ascii().get_data());
}


int N2NVPN::start_network() {
	int                       rc;
	tuntap_dev                tuntap;
	n2n_tuntap_priv_config_t  ec;
	int                       runlevel;
	time_t                    now;
	time_t                    last_action;
	fd_set                    socket_mask;
	struct timeval            wait_time;
	int                       seek_answer;
	uint16_t                  expected;
	uint16_t                  position;
	unsigned char             pktbuf[N2N_SN_PKTBUF_SIZE + sizeof(uint16_t)];
	peer_info_t              *scan;
	peer_info_t              *scan_tmp;

	if (edge_verify_conf(&_conf) != 0) {
		return -1;
	}

	_eee = edge_init(&_conf, &rc);
	if (_eee == nullptr) {
		return -3;
	}

	memset(&ec, 0, sizeof(ec));
	ec.mtu    = DEFAULT_MTU;
#ifndef _WIN32
	snprintf(ec.tuntap_dev_name, sizeof(ec.tuntap_dev_name), N2N_EDGE_DEFAULT_DEV_NAME);
#endif
	snprintf(ec.netmask, sizeof(ec.netmask), N2N_EDGE_DEFAULT_NETMASK);

	// This doesn't handle actual DHCP.
	if (_ip_address == "") {
		// Automatic.
		_eee->conf.tuntap_ip_mode = TUNTAP_IP_MODE_SN_ASSIGN;
	} else {
	// Static
		_eee->conf.tuntap_ip_mode = TUNTAP_IP_MODE_STATIC;
		snprintf((char *)&ec.ip_mode, N2N_IF_MODE_SIZE, "static");
		snprintf((char *)&ec.ip_addr, N2N_NETMASK_STR_SIZE, "%s", _ip_address.ascii().get_data());
	}

	memcpy(&(_eee->tuntap_priv_conf), &ec, sizeof(ec));

	expected       = sizeof(uint16_t);
	position       = 0;
	runlevel       = 0;
	seek_answer    = 1;
	_eee->last_sup = 0;
	_eee->curr_sn  = _eee->conf.supernodes;
	supernode_connect(_eee);

	print_line("Runlevel 0");

	while (runlevel < 5) {

		now = time(nullptr);

		// we do not use switch-case because we also check for 'greater than'

		if (runlevel == 0) { /* PING to all known supernodes */
			last_action = now;
			_eee->sn_pong = 0;
			// (re-)initialize the number of max concurrent pings (decreases by calling send_query_peer)
			_eee->conf.number_max_sn_pings = NUMBER_SN_PINGS_INITIAL;
			send_query_peer(_eee, null_mac);
			runlevel++;
			print_line("Runlevel", runlevel);
		}

		if (runlevel == 1) { /* PING has been sent to all known supernodes */
			if (_eee->sn_pong) {
				// first answer
				_eee->sn_pong = 0;
				sn_selection_sort(&(_eee->conf.supernodes));
				_eee->curr_sn = _eee->conf.supernodes;
				supernode_connect(_eee);
				runlevel++;
				print_line("Runlevel", runlevel);
			} else if (last_action <= (now - BOOTSTRAP_TIMEOUT)) {
				// timeout
				runlevel--;
				print_line("Runlevel", runlevel);
				// skip waiting for answer to directly go to send PING again
				seek_answer = 0;
			}
		}

		// by the way, have every later PONG cause the remaining (!) list to be sorted because the entries
		// before have already been tried; as opposed to initial PONG, do not change curr_sn
		if (runlevel > 1) {
			if (_eee->sn_pong) {
				_eee->sn_pong = 0;
				if (_eee->curr_sn->hh.next) {
					sn_selection_sort((peer_info_t**)&(_eee->curr_sn->hh.next));
					// here, it is hard to determine from which one, so no details to output
				}
			}
		}

		if (runlevel == 2) { /* send REGISTER_SUPER to get auto ip address from a supernode */
			if (_eee->conf.tuntap_ip_mode == TUNTAP_IP_MODE_SN_ASSIGN) {
				last_action = now;
				_eee->sn_wait = 1;
				send_register_super(_eee);
				runlevel++;
				print_line("Runlevel", runlevel);
			} else {
				runlevel += 2; /* skip waiting for TUNTAP IP address */
				print_line("Runlevel", runlevel);
			}
		}

		if (runlevel == 3) { /* REGISTER_SUPER to get auto ip address from a sn has been sent */
			if (!_eee->sn_wait) { /* TUNTAP IP address received */
				runlevel++;
				print_line("Runlevel", runlevel);
				// it should be from curr_sn, but we can't determine definitely here, so no details to output
			} else if (last_action <= (now - BOOTSTRAP_TIMEOUT)) {
				// timeout, so try next supernode
				if (_eee->curr_sn->hh.next)
					_eee->curr_sn = (peer_info *)_eee->curr_sn->hh.next;
				else
					_eee->curr_sn = _eee->conf.supernodes;
				supernode_connect(_eee);
				runlevel--;
				print_line("Runlevel", runlevel);
				// skip waiting for answer to direcly go to send REGISTER_SUPER again
				seek_answer = 0;
			}
		}

		if (runlevel == 4) { /* configure the TUNTAP device, including routes */

			print_line("ec:");
			print_line("dev_name:",ec.tuntap_dev_name);
			print_line(" ip_mode:",ec.ip_mode        );
			print_line(" netmask:",ec.netmask        );
			print_line(" ip_addr:",ec.ip_addr        );
			print_line("     mac:",ec.device_mac     );
			print_line("     mtu:",ec.mtu            );
			print_line("  metric:",ec.metric         );

			print_line("_eee->tuntap_priv_conf:");
			print_line("dev_name:",_eee->tuntap_priv_conf.tuntap_dev_name);
			print_line(" ip_mode:",_eee->tuntap_priv_conf.ip_mode        );
			print_line(" netmask:",_eee->tuntap_priv_conf.netmask        );
			print_line(" ip_addr:",_eee->tuntap_priv_conf.ip_addr        );
			print_line("     mac:",_eee->tuntap_priv_conf.device_mac     );
			print_line("     mtu:",_eee->tuntap_priv_conf.mtu            );
			print_line("  metric:",_eee->tuntap_priv_conf.metric         );

			if (tuntap_open(&tuntap,
					_eee->tuntap_priv_conf.tuntap_dev_name,
					_eee->tuntap_priv_conf.ip_mode,
					_eee->tuntap_priv_conf.ip_addr,
					_eee->tuntap_priv_conf.netmask,
					_eee->tuntap_priv_conf.device_mac,
					_eee->tuntap_priv_conf.mtu,
					_eee->tuntap_priv_conf.metric) < 0) return -2;

			memcpy(&_eee->device, &tuntap, sizeof(tuntap));

			runlevel = 5;
			print_line("Runlevel",runlevel);
			// no more answers required
			seek_answer = 0;
		}

		// we usually wait for some answer, there however are exceptions when going back to a previous runlevel
		if (seek_answer) {
			FD_ZERO(&socket_mask);
			FD_SET(_eee->sock, &socket_mask);
			wait_time.tv_sec = BOOTSTRAP_TIMEOUT;
			wait_time.tv_usec = 0;

			if (select(_eee->sock + 1, &socket_mask, nullptr, nullptr, &wait_time) > 0) {
				if (FD_ISSET(_eee->sock, &socket_mask)) {
					fetch_and_eventually_process_data(_eee, _eee->sock, pktbuf, &expected, &position, now);
				}
			}
		}
		seek_answer = 1;

		resolve_check(_eee->resolve_parameter, 0 /* no intermediate resolution requirement at this point */, now);
	}

	// allow a higher number of pings for first regular round of ping
	// to quicker get an inital 'supernode selection criterion overview'
	_eee->conf.number_max_sn_pings = NUMBER_SN_PINGS_INITIAL;

	// shape supernode list; make current one the first on the list
	HASH_ITER(hh, _eee->conf.supernodes, scan, scan_tmp) {
		if (scan == _eee->curr_sn)
			sn_selection_criterion_good(&(scan->selection_criterion));
		else
			sn_selection_criterion_default(&(scan->selection_criterion));
	}
	sn_selection_sort(&(_eee->conf.supernodes));

	// do not immediately ping again, allow some time
	_eee->last_sweep        = now - SWEEP_TIME + 2 * BOOTSTRAP_TIMEOUT;
	_eee->sn_wait           = 1;
	_eee->last_register_req = 0;

	_keep_running = true;
	_eee->keep_running = &_keep_running;

	_edge_thread = new std::thread(&N2NVPN::_edge_thread_function, this);

	return 0;
}


int N2NVPN::stop_network() {
	print_line("Stopping thread");
	_keep_running = false;
	print_line("Joining thread");
	_edge_thread->join();
	print_line("Joined");

	return 0;
}
