#include "bsp/board_api.h"
#include "tusb.h"
#include "dhserver.h"
#include "dnserver.h"
#include "lwip/apps/httpd.h"
#include "lwip/init.h"
#include "lwip/timeouts.h"

#include "websocket.h"
#include "usb_device.h"
#include "aw410k.h"

#include "usb_server.h"

// ip address of the USB server and dhcp address(es) it will give out
static const ip4_addr_t usb_ip = INIT_IP4(192, 168, 40, 1);
static const ip4_addr_t usb_netmask = INIT_IP4(255, 255, 255, 0);
static const ip4_addr_t usb_gateway = INIT_IP4(0, 0, 0, 0);
static dhcp_entry_t dhcp_clients[] = {
	{ {0}, INIT_IP4(192, 168, 40, 2), 4*3600 },
};
static const dhcp_config_t dhcp_config = {
	.router = INIT_IP4(0,0,0,0),
	.port = 67,
	.dns = usb_ip,
	"usb",
	TU_ARRAY_SIZE(dhcp_clients),
	dhcp_clients
};

static struct netif netif_data;

static err_t netif_init_cb(struct netif *netif);
static err_t ip4_output_fn(struct netif *netif, struct pbuf *p, const ip4_addr_t *addr);
static err_t linkoutput_fn(struct netif *netif, struct pbuf *p);
static void usb_server_netif_link_cb(struct netif *netif);
static bool dns_request(const char *name, ip4_addr_t *addr);

// called to initialize the USB network and HTTP server
void usb_server_init(void) {
	struct netif *netif = &netif_data;

	lwip_init();

	// use 02 followed by last 10 digits from board serial as MAC address
	uint8_t board_serial[16];
	size_t count = board_get_unique_id(board_serial, sizeof(board_serial));
	netif->hwaddr_len = 6;
	memcpy(netif->hwaddr, &board_serial[count-6], 6);
	netif->hwaddr[0]=0x02;
	// lwip virtual MAC address msut differ from the host MAC - toggle last bit
	netif->hwaddr[5] ^= 0x01;

	netif = netif_add(netif, &usb_ip, &usb_netmask, &usb_gateway, NULL, netif_init_cb, ethernet_input);
	netif_set_default(netif);

#if LWIP_NETIF_LINK_CALLBACK
	netif_set_link_callback(netif, usb_server_netif_link_cb);
	netif_set_link_up(netif);
#else
	//tud_network_link_state(BOARD_TUD_RHPORT, true);
	// unsupported in current version - add when Pico SDK updates TinyUSB version
#endif

	while (!netif_is_up(&netif_data));
	while (dhserv_init(&dhcp_config) != ERR_OK);
	while (dnserv_init(IP_ADDR_ANY, 53, dns_request) != ERR_OK);
	
	httpd_init();

	// start the websocket server
	ws_server_init();
	ws_set_open_handler(ws_open_handler);
	ws_set_receive_handler(ws_receive_handler);
}

// callback when data is received on USB network
// return true if the packet buffer was accepted
bool tud_network_recv_cb(const uint8_t *src, uint16_t size) {
	struct netif *netif = &netif_data;

	if (size) {
		struct pbuf *p = pbuf_alloc(PBUF_RAW, size, PBUF_POOL);

		if (p == NULL) {
			printf("ERROR: Failed to allocate pbuf of size %d\n", size);
			return false;
		}

		/* Copy buf to pbuf */
		pbuf_take(p, src, size);

		// Surrender ownership of our pbuf unless there was an error
		// Only call pbuf_free if not Ok else it will panic with "pbuf_free: p->ref > 0"
		// or steal it from whatever took ownership of it with undefined consequences.
		// See: https://savannah.nongnu.org/patch/index.php?10121
		if (netif->input(p, netif) != ERR_OK) {
			printf("ERROR: netif input failed\n");
			pbuf_free(p);
		}
		// Signal tinyusb that the current frame has been processed.
		tud_network_recv_renew();
	}

	return true;
}

// callback when network interface is initialized
// save the network configuration
static err_t netif_init_cb(struct netif *netif) {
	LWIP_ASSERT("netif != NULL", (netif != NULL));
	netif->mtu = CFG_TUD_NET_MTU;
	netif->flags = NETIF_FLAG_BROADCAST | NETIF_FLAG_ETHARP | NETIF_FLAG_LINK_UP | NETIF_FLAG_UP;
	netif->state = NULL;
	netif->name[0] = 'E';
	netif->name[1] = 'X';
	netif->linkoutput = linkoutput_fn;
	netif->output = ip4_output_fn;

	return ERR_OK;
}

// callback for sending data over USB network interface
// copy from network stack packet pointer to dst
uint16_t tud_network_xmit_cb(uint8_t *dst, void *ref, uint16_t arg) {
	struct pbuf *p = (struct pbuf *) ref;

	(void) arg; /* unused for this example */

	return pbuf_copy_partial(p, dst, p->tot_len, 0);
}

static err_t ip4_output_fn(struct netif *netif, struct pbuf *p, const ip4_addr_t *addr) {
	return etharp_output(netif, p, addr);
}

static err_t linkoutput_fn(struct netif *netif, struct pbuf *p) {
	(void) netif;

	for (;;) {
		// if TinyUSB isn't ready, we must signal back to lwip that there is nothing we can do
		if (!tud_ready())
			return ERR_USE;

		// if the network driver can accept another packet, we make it happen
		if (tud_network_can_xmit(p->tot_len)) {
			tud_network_xmit(p, 0 /* unused for this example */);
			return ERR_OK;
		}

		// transfer execution to TinyUSB in the hopes that it will finish transmitting the prior packet
		tud_task();
	}
}

// notify USB host about link state changes
static void usb_server_netif_link_cb(struct netif *netif) {
	bool link_up = netif_is_link_up(netif);
	//tud_network_link_state(BOARD_TUD_RHPORT, link_up);
	// unsupported in current version - add when Pico SDK updates TinyUSB version
}

// handle DNS requests and serve on designed domain
static bool dns_request(const char *name, ip4_addr_t *addr) {
	if (0 == strcmp(name, "aw410k.usb")) {
		*addr = usb_ip;
		return true;
	}
	return false;
}

// handler called when websocket connection is opened
const void ws_open_handler(struct ws_state * wss) {
	(void) wss;

	// nothing to do
}

// handler for data received on websocket connection
const void ws_receive_handler(uint8_t *data, uint16_t len) {
	if (strncmp(data, "S,", 2) == 0) {
		// set color command
		parse_colors(&data[2], len-2);
	} else if ( strncmp(data, "G,", 2) == 0) {
		// get color comand
		get_color(&data[2], len-2);
	} else if ( strncmp(data, "F,", 2) == 0) {
		// save to flash memory
		save_rgb_config();
	} else if ( strncmp(data, "L,", 2) == 0) {
		// load from flash memory
		load_rgb_config();
	}
}

// Pico specific routines needed by lwip
auto_init_mutex(lwip_mutex);
static int lwip_mutex_count = 0;

sys_prot_t sys_arch_protect(void)
{
	uint32_t owner;
	if (!mutex_try_enter(&lwip_mutex, &owner))
	{
		if (owner != get_core_num())
		{
			// Wait until other core releases mutex
			mutex_enter_blocking(&lwip_mutex);
		}
	}

	lwip_mutex_count++;
	
	return 0;
}

void sys_arch_unprotect(sys_prot_t pval)
{
	(void)pval;
	
	if (lwip_mutex_count)
	{
		lwip_mutex_count--;
		if (!lwip_mutex_count)
		{
			mutex_exit(&lwip_mutex);
		}
	}
}

uint32_t sys_now(void)
{
	return to_ms_since_boot( get_absolute_time() );
}
