/*
 * Copyright (c) 2023 Nordic Semiconductor ASA
 *
 * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause
 */

#include <zephyr/device.h>
#include <zephyr/devicetree.h>
#include <zephyr/kernel.h>
#include <zephyr/logging/log.h>
#include <zephyr/zbus/zbus.h>
#include <zephyr/net/net_if.h>
#include <zephyr/net/net_ip.h>
#include <zephyr/net/net_mgmt.h>
#if defined(CONFIG_NET_DHCPV4)
#include <zephyr/net/dhcpv4.h>
#endif
#include <zephyr/net/net_config.h>
#include <string.h>

#include "message_channel.h"
#include "common/system_state.h"

/* Register log module */
LOG_MODULE_REGISTER(network, CONFIG_MQTT_SAMPLE_NETWORK_LOG_LEVEL);

/* This module does not subscribe to any channels */

#define IPV4_EVENT_MASK  (NET_EVENT_IPV4_ADDR_ADD | NET_EVENT_IPV4_ADDR_DEL)
#define IFACE_EVENT_MASK (NET_EVENT_IF_UP | NET_EVENT_IF_DOWN)

#define W5500_NODE DT_NODELABEL(w5500)

#if !DT_NODE_EXISTS(W5500_NODE)
#error "W5500 node not found in devicetree"
#endif

static struct net_mgmt_event_callback ipv4_cb;
static struct net_mgmt_event_callback iface_cb;
static struct net_if *w5500_iface;
static enum network_status last_status = NETWORK_DISCONNECTED;

static void publish_status(enum network_status status)
{
	int err;

	if (status == last_status) {
		return;
	}

	LOG_INF("Network status: %s",
		(status == NETWORK_CONNECTED) ? "connected" : "disconnected");

	err = zbus_chan_pub(&NETWORK_CHAN, &status, K_SECONDS(1));
	if (err) {
		LOG_ERR("zbus_chan_pub, error: %d", err);
		SEND_FATAL_ERROR();
		return;
	}

	last_status = status;
	system_state_set_network_ready(status == NETWORK_CONNECTED);
}

static void ipv4_event_handler(struct net_mgmt_event_callback *cb,
			       uint32_t event,
			       struct net_if *iface)
{
	if (iface != w5500_iface) {
		return;
	}

	switch (event) {
	case NET_EVENT_IPV4_ADDR_ADD:
		LOG_INF("IPv4 address configured");
		if (net_if_ipv4_get_global_addr(iface, NET_ADDR_PREFERRED)) {
			publish_status(NETWORK_CONNECTED);
		}
		break;
	case NET_EVENT_IPV4_ADDR_DEL:
		if (!net_if_ipv4_get_global_addr(iface, NET_ADDR_PREFERRED)) {
			LOG_INF("IPv4 address removed");
			publish_status(NETWORK_DISCONNECTED);
		}
		break;
	default:
		break;
	}
}

static void iface_event_handler(struct net_mgmt_event_callback *cb,
				uint32_t event,
				struct net_if *iface)
{
	if (iface != w5500_iface) {
		return;
	}

	switch (event) {
	case NET_EVENT_IF_UP:
		LOG_INF("Interface is up");
		if (net_if_ipv4_get_global_addr(iface, NET_ADDR_PREFERRED)) {
			publish_status(NETWORK_CONNECTED);
		}
		break;
	case NET_EVENT_IF_DOWN:
		LOG_WRN("Interface is down");
		publish_status(NETWORK_DISCONNECTED);
		break;
	default:
		break;
	}
}

static void network_task(void)
{
	int err;
	const struct device *const w5500_dev = DEVICE_DT_GET(W5500_NODE);

	LOG_INF("Network task starting");

	system_state_set_network_ready(false);

	if (!device_is_ready(w5500_dev)) {
		LOG_ERR("W5500 device not ready");
		SEND_FATAL_ERROR();
		return;
	}

	w5500_iface = net_if_lookup_by_dev(w5500_dev);
	if (!w5500_iface) {
		LOG_ERR("Failed to map W5500 device to network interface");
		SEND_FATAL_ERROR();
		return;
	}

	net_if_set_default(w5500_iface);

	LOG_INF("Using network interface: %s", w5500_dev->name);

	net_mgmt_init_event_callback(&ipv4_cb, ipv4_event_handler, IPV4_EVENT_MASK);
	net_mgmt_add_event_callback(&ipv4_cb);

	net_mgmt_init_event_callback(&iface_cb, iface_event_handler, IFACE_EVENT_MASK);
	net_mgmt_add_event_callback(&iface_cb);

	LOG_INF("Bringing Ethernet interface up");

	if (!net_if_is_up(w5500_iface)) {
		err = net_if_up(w5500_iface);
		if (err) {
			LOG_ERR("net_if_up failed: %d", err);
			SEND_FATAL_ERROR();
			return;
		} else {
			LOG_INF("net_if_up succeeded");
		}
	}

#if defined(CONFIG_NET_DHCPV4)
	if (IS_ENABLED(CONFIG_NET_DHCPV4)) {
		LOG_INF("Starting DHCPv4 client");
		err = net_dhcpv4_start(w5500_iface);
		if (err) {
			LOG_ERR("Failed to start DHCPv4: %d", err);
		}
	}
#endif

#if defined(CONFIG_NET_CONFIG_SETTINGS) && !defined(CONFIG_NET_DHCPV4)
	struct in_addr addr;
	struct in_addr netmask;
	struct in_addr gw;
	bool addr_configured = false;

	if (strlen(CONFIG_NET_CONFIG_MY_IPV4_ADDR) > 0 &&
	    net_addr_pton(AF_INET, CONFIG_NET_CONFIG_MY_IPV4_ADDR, &addr) == 0) {
		struct net_if *lookup_iface = NULL;

		if (!net_if_ipv4_addr_lookup(&addr, &lookup_iface)) {
			net_if_ipv4_addr_add(w5500_iface, &addr, NET_ADDR_MANUAL, 0);
			LOG_INF("Configured static IPv4 address: %s", CONFIG_NET_CONFIG_MY_IPV4_ADDR);
			addr_configured = true;
		} else if (lookup_iface == w5500_iface) {
			addr_configured = true;
		} else {
			LOG_WRN("IPv4 address already configured on another interface");
		}
	}

	if (addr_configured &&
	    strlen(CONFIG_NET_CONFIG_MY_IPV4_NETMASK) > 0 &&
	    net_addr_pton(AF_INET, CONFIG_NET_CONFIG_MY_IPV4_NETMASK, &netmask) == 0) {
		net_if_ipv4_set_netmask_by_addr(w5500_iface, &addr, &netmask);
	}

	if (strlen(CONFIG_NET_CONFIG_MY_IPV4_GW) > 0 &&
	    net_addr_pton(AF_INET, CONFIG_NET_CONFIG_MY_IPV4_GW, &gw) == 0) {
		net_if_ipv4_set_gw(w5500_iface, &gw);
		LOG_INF("Configured IPv4 gateway: %s", CONFIG_NET_CONFIG_MY_IPV4_GW);
	}
#endif

	if (net_if_ipv4_get_global_addr(w5500_iface, NET_ADDR_PREFERRED)) {
		publish_status(NETWORK_CONNECTED);
	} else {
		LOG_INF("Waiting for IPv4 address assignment");
	}

	k_sleep(K_FOREVER);
}

K_THREAD_DEFINE(network_task_id,
		CONFIG_MQTT_SAMPLE_NETWORK_THREAD_STACK_SIZE,
		network_task, NULL, NULL, NULL, 3, 0, 0);
