/*
 * Copyright (c) 2012 Jiri Svoboda
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * - Redistributions of source code must retain the above copyright
 *   notice, this list of conditions and the following disclaimer.
 * - Redistributions in binary form must reproduce the above copyright
 *   notice, this list of conditions and the following disclaimer in the
 *   documentation and/or other materials provided with the distribution.
 * - The name of the author may not be used to endorse or promote products
 *   derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
 * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
 * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

/** @addtogroup ethip
 * @{
 */
/**
 * @file
 * @brief
 */

#include <adt/list.h>
#include <async.h>
#include <stdbool.h>
#include <errno.h>
#include <fibril_synch.h>
#include <inet/iplink_srv.h>
#include <io/log.h>
#include <loc.h>
#include <device/nic.h>
#include <stdlib.h>
#include <mem.h>
#include "ethip.h"
#include "ethip_nic.h"
#include "pdu.h"

static int ethip_nic_open(service_id_t sid);
static void ethip_nic_cb_conn(ipc_callid_t iid, ipc_call_t *icall, void *arg);

static LIST_INITIALIZE(ethip_nic_list);
static FIBRIL_MUTEX_INITIALIZE(ethip_discovery_lock);

static int ethip_nic_check_new(void)
{
	bool already_known;
	category_id_t iplink_cat;
	service_id_t *svcs;
	size_t count, i;
	int rc;

	fibril_mutex_lock(&ethip_discovery_lock);

	rc = loc_category_get_id("nic", &iplink_cat, IPC_FLAG_BLOCKING);
	if (rc != EOK) {
		log_msg(LOG_DEFAULT, LVL_ERROR, "Failed resolving category 'nic'.");
		fibril_mutex_unlock(&ethip_discovery_lock);
		return ENOENT;
	}

	rc = loc_category_get_svcs(iplink_cat, &svcs, &count);
	if (rc != EOK) {
		log_msg(LOG_DEFAULT, LVL_ERROR, "Failed getting list of IP links.");
		fibril_mutex_unlock(&ethip_discovery_lock);
		return EIO;
	}

	for (i = 0; i < count; i++) {
		already_known = false;

		list_foreach(ethip_nic_list, link, ethip_nic_t, nic) {
			if (nic->svc_id == svcs[i]) {
				already_known = true;
				break;
			}
		}

		if (!already_known) {
			log_msg(LOG_DEFAULT, LVL_DEBUG, "Found NIC '%lu'",
			    (unsigned long) svcs[i]);
			rc = ethip_nic_open(svcs[i]);
			if (rc != EOK)
				log_msg(LOG_DEFAULT, LVL_ERROR, "Could not open NIC.");
		}
	}

	fibril_mutex_unlock(&ethip_discovery_lock);
	return EOK;
}

static ethip_nic_t *ethip_nic_new(void)
{
	ethip_nic_t *nic = calloc(1, sizeof(ethip_nic_t));
	if (nic == NULL) {
		log_msg(LOG_DEFAULT, LVL_ERROR, "Failed allocating NIC structure. "
		    "Out of memory.");
		return NULL;
	}
	
	link_initialize(&nic->link);
	list_initialize(&nic->addr_list);
	
	return nic;
}

static ethip_link_addr_t *ethip_nic_addr_new(inet_addr_t *addr)
{
	ethip_link_addr_t *laddr = calloc(1, sizeof(ethip_link_addr_t));
	if (laddr == NULL) {
		log_msg(LOG_DEFAULT, LVL_ERROR, "Failed allocating NIC address structure. "
		    "Out of memory.");
		return NULL;
	}
	
	link_initialize(&laddr->link);
	laddr->addr = *addr;
	
	return laddr;
}

static void ethip_nic_delete(ethip_nic_t *nic)
{
	if (nic->svc_name != NULL)
		free(nic->svc_name);
	
	free(nic);
}

static void ethip_link_addr_delete(ethip_link_addr_t *laddr)
{
	free(laddr);
}

static int ethip_nic_open(service_id_t sid)
{
	bool in_list = false;
	nic_address_t nic_address;
	
	log_msg(LOG_DEFAULT, LVL_DEBUG, "ethip_nic_open()");
	ethip_nic_t *nic = ethip_nic_new();
	if (nic == NULL)
		return ENOMEM;
	
	int rc = loc_service_get_name(sid, &nic->svc_name);
	if (rc != EOK) {
		log_msg(LOG_DEFAULT, LVL_ERROR, "Failed getting service name.");
		goto error;
	}
	
	nic->sess = loc_service_connect(EXCHANGE_SERIALIZE, sid, 0);
	if (nic->sess == NULL) {
		log_msg(LOG_DEFAULT, LVL_ERROR, "Failed connecting '%s'", nic->svc_name);
		goto error;
	}
	
	nic->svc_id = sid;
	
	rc = nic_callback_create(nic->sess, ethip_nic_cb_conn, nic);
	if (rc != EOK) {
		log_msg(LOG_DEFAULT, LVL_ERROR, "Failed creating callback connection "
		    "from '%s'", nic->svc_name);
		goto error;
	}

	log_msg(LOG_DEFAULT, LVL_DEBUG, "Opened NIC '%s'", nic->svc_name);
	list_append(&nic->link, &ethip_nic_list);
	in_list = true;

	rc = ethip_iplink_init(nic);
	if (rc != EOK)
		goto error;

	rc = nic_get_address(nic->sess, &nic_address);
	if (rc != EOK) {
		log_msg(LOG_DEFAULT, LVL_ERROR, "Error getting MAC address of NIC '%s'.",
		    nic->svc_name);
		goto error;
	}
	
	addr48(nic_address.address, nic->mac_addr);

	rc = nic_set_state(nic->sess, NIC_STATE_ACTIVE);
	if (rc != EOK) {
		log_msg(LOG_DEFAULT, LVL_ERROR, "Error activating NIC '%s'.",
		    nic->svc_name);
		goto error;
	}

	rc = nic_broadcast_set_mode(nic->sess, NIC_BROADCAST_ACCEPTED);
	if (rc != EOK) {
		log_msg(LOG_DEFAULT, LVL_ERROR, "Error enabling "
		    "reception of broadcast frames on '%s'.", nic->svc_name);
		goto error;
	}

	log_msg(LOG_DEFAULT, LVL_DEBUG, "Initialized IP link service,");

	return EOK;

error:
	if (in_list)
		list_remove(&nic->link);
	
	if (nic->sess != NULL)
		async_hangup(nic->sess);
	
	ethip_nic_delete(nic);
	return rc;
}

static void ethip_nic_cat_change_cb(void)
{
	(void) ethip_nic_check_new();
}

static void ethip_nic_addr_changed(ethip_nic_t *nic, ipc_callid_t callid,
    ipc_call_t *call)
{
	log_msg(LOG_DEFAULT, LVL_DEBUG, "ethip_nic_addr_changed()");
	async_answer_0(callid, ENOTSUP);
}

static void ethip_nic_received(ethip_nic_t *nic, ipc_callid_t callid,
    ipc_call_t *call)
{
	int rc;
	void *data;
	size_t size;

	log_msg(LOG_DEFAULT, LVL_DEBUG, "ethip_nic_received() nic=%p", nic);

	rc = async_data_write_accept(&data, false, 0, 0, 0, &size);
	if (rc != EOK) {
		log_msg(LOG_DEFAULT, LVL_DEBUG, "data_write_accept() failed");
		return;
	}

	log_msg(LOG_DEFAULT, LVL_DEBUG, "Ethernet PDU contents (%zu bytes)",
	    size);

	log_msg(LOG_DEFAULT, LVL_DEBUG, "call ethip_received");
	rc = ethip_received(&nic->iplink, data, size);
	log_msg(LOG_DEFAULT, LVL_DEBUG, "free data");
	free(data);

	log_msg(LOG_DEFAULT, LVL_DEBUG, "ethip_nic_received() done, rc=%d", rc);
	async_answer_0(callid, rc);
}

static void ethip_nic_device_state(ethip_nic_t *nic, ipc_callid_t callid,
    ipc_call_t *call)
{
	log_msg(LOG_DEFAULT, LVL_DEBUG, "ethip_nic_device_state()");
	async_answer_0(callid, ENOTSUP);
}

static void ethip_nic_cb_conn(ipc_callid_t iid, ipc_call_t *icall, void *arg)
{
	ethip_nic_t *nic = (ethip_nic_t *)arg;

	log_msg(LOG_DEFAULT, LVL_DEBUG, "ethnip_nic_cb_conn()");

	while (true) {
		ipc_call_t call;
		ipc_callid_t callid = async_get_call(&call);

		if (!IPC_GET_IMETHOD(call)) {
			/* TODO: Handle hangup */
			return;
		}

		switch (IPC_GET_IMETHOD(call)) {
		case NIC_EV_ADDR_CHANGED:
			ethip_nic_addr_changed(nic, callid, &call);
			break;
		case NIC_EV_RECEIVED:
			ethip_nic_received(nic, callid, &call);
			break;
		case NIC_EV_DEVICE_STATE:
			ethip_nic_device_state(nic, callid, &call);
			break;
		default:
			async_answer_0(callid, ENOTSUP);
		}
	}
}

int ethip_nic_discovery_start(void)
{
	int rc = loc_register_cat_change_cb(ethip_nic_cat_change_cb);
	if (rc != EOK) {
		log_msg(LOG_DEFAULT, LVL_ERROR, "Failed registering callback for NIC "
		    "discovery (%d).", rc);
		return rc;
	}
	
	return ethip_nic_check_new();
}

ethip_nic_t *ethip_nic_find_by_iplink_sid(service_id_t iplink_sid)
{
	log_msg(LOG_DEFAULT, LVL_DEBUG, "ethip_nic_find_by_iplink_sid(%u)",
	    (unsigned) iplink_sid);

	list_foreach(ethip_nic_list, link, ethip_nic_t, nic) {
		log_msg(LOG_DEFAULT, LVL_DEBUG, "ethip_nic_find_by_iplink_sid - element");
		if (nic->iplink_sid == iplink_sid) {
			log_msg(LOG_DEFAULT, LVL_DEBUG, "ethip_nic_find_by_iplink_sid - found %p", nic);
			return nic;
		}
	}

	log_msg(LOG_DEFAULT, LVL_DEBUG, "ethip_nic_find_by_iplink_sid - not found");
	return NULL;
}

int ethip_nic_send(ethip_nic_t *nic, void *data, size_t size)
{
	int rc;
	log_msg(LOG_DEFAULT, LVL_DEBUG, "ethip_nic_send(size=%zu)", size);
	rc = nic_send_frame(nic->sess, data, size);
	log_msg(LOG_DEFAULT, LVL_DEBUG, "nic_send_frame -> %d", rc);
	return rc;
}

/** Setup accepted multicast addresses
 *
 * Currently the set of accepted multicast addresses is
 * determined only based on IPv6 addresses.
 *
 */
static int ethip_nic_setup_multicast(ethip_nic_t *nic)
{
	log_msg(LOG_DEFAULT, LVL_DEBUG, "ethip_nic_setup_multicast()");
	
	/* Count the number of multicast addresses */
	
	size_t count = 0;
	
	list_foreach(nic->addr_list, link, ethip_link_addr_t, laddr) {
		ip_ver_t ver = inet_addr_get(&laddr->addr, NULL, NULL);
		if (ver == ip_v6)
			count++;
	}
	
	if (count == 0)
		return nic_multicast_set_mode(nic->sess, NIC_MULTICAST_BLOCKED,
		    NULL, 0);
	
	nic_address_t *mac_list = calloc(count, sizeof(nic_address_t));
	if (mac_list == NULL)
		return ENOMEM;
	
	/* Create the multicast MAC list */
	
	size_t i = 0;
	
	list_foreach(nic->addr_list, link, ethip_link_addr_t, laddr) {
		addr128_t v6;
		ip_ver_t ver = inet_addr_get(&laddr->addr, NULL, &v6);
		if (ver != ip_v6)
			continue;
		
		assert(i < count);
		
		addr48_t mac;
		addr48_solicited_node(v6, mac);
		
		/* Avoid duplicate addresses in the list */
		
		bool found = false;
		
		for (size_t j = 0; j < i; j++) {
			if (addr48_compare(mac_list[j].address, mac)) {
				found = true;
				break;
			}
		}
		
		if (!found) {
			addr48(mac, mac_list[i].address);
			i++;
		} else
			count--;
	}
	
	/* Setup the multicast MAC list */
	
	int rc = nic_multicast_set_mode(nic->sess, NIC_MULTICAST_LIST,
	    mac_list, count);
	
	free(mac_list);
	return rc;
}

int ethip_nic_addr_add(ethip_nic_t *nic, inet_addr_t *addr)
{
	log_msg(LOG_DEFAULT, LVL_DEBUG, "ethip_nic_addr_add()");
	
	ethip_link_addr_t *laddr = ethip_nic_addr_new(addr);
	if (laddr == NULL)
		return ENOMEM;
	
	list_append(&laddr->link, &nic->addr_list);
	
	return ethip_nic_setup_multicast(nic);
}

int ethip_nic_addr_remove(ethip_nic_t *nic, inet_addr_t *addr)
{
	log_msg(LOG_DEFAULT, LVL_DEBUG, "ethip_nic_addr_remove()");
	
	ethip_link_addr_t *laddr = ethip_nic_addr_find(nic, addr);
	if (laddr == NULL)
		return ENOENT;
	
	list_remove(&laddr->link);
	ethip_link_addr_delete(laddr);
	
	return ethip_nic_setup_multicast(nic);
}

ethip_link_addr_t *ethip_nic_addr_find(ethip_nic_t *nic,
    inet_addr_t *addr)
{
	log_msg(LOG_DEFAULT, LVL_DEBUG, "ethip_nic_addr_find()");
	
	list_foreach(nic->addr_list, link, ethip_link_addr_t, laddr) {
		if (inet_addr_compare(addr, &laddr->addr))
			return laddr;
	}
	
	return NULL;
}

/** @}
 */
