/*
 * Copyright (c) 2009 Intel Corporation.  All rights reserved.
 * Copyright (c) 2013 Mellanox Technologies LTD. All rights reserved.
 *
 * This software is available to you under the OpenIB.org BSD license
 * below:
 *
 *     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 SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AWV
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
 * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
 * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
 * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

#include <config.h>

#include <osd.h>
#include "libacm.h"
#include <infiniband/acm.h>
#include <stdio.h>
#include <errno.h>
#include <netdb.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/un.h>

#include <util/util.h>

static pthread_mutex_t acm_lock = PTHREAD_MUTEX_INITIALIZER;
static int sock = -1;
static short server_port = 6125;

static void acm_set_server_port(void)
{
	FILE *f;

	f = fopen(IBACM_IBACME_PORT_FILE, "r");
	if (f) {
		if (fscanf(f, "%hu", (unsigned short *) &server_port) != 1)
			printf("Failed to read server port\n");
		fclose(f);
	}
}

static int ib_acm_connect_open(char *dest)
{
	struct addrinfo hint, *res;
	int ret;

	acm_set_server_port();
	memset(&hint, 0, sizeof hint);

	hint.ai_family = AF_UNSPEC;
	hint.ai_protocol = IPPROTO_TCP;

	ret = getaddrinfo(dest, NULL, &hint, &res);
	if (ret)
		return ret;

	sock = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
	if (sock == -1) {
		ret = errno;
		goto freeaddr;
	}

	((struct sockaddr_in *) res->ai_addr)->sin_port = htobe16(server_port);
	ret = connect(sock, res->ai_addr, res->ai_addrlen);
	if (ret) {
		close(sock);
		sock = -1;
	}

freeaddr:
	freeaddrinfo(res);
	return ret;
}

static int ib_acm_connect_unix(char *dest)
{
	struct sockaddr_un addr;
	int ret;

	addr.sun_family = AF_UNIX;
	if (dest) {
		if (snprintf(addr.sun_path, sizeof(addr.sun_path),
			     "%s", dest) >= sizeof(addr.sun_path)) {
			errno = ENAMETOOLONG;
			return errno;
		}
	} else {
		BUILD_ASSERT(sizeof(IBACM_IBACME_SERVER_PATH) <=
			     sizeof(addr.sun_path));
		strcpy(addr.sun_path, IBACM_IBACME_SERVER_PATH);
	}

	sock = socket(AF_UNIX, SOCK_STREAM, 0);
	if (sock < 0)
		return errno;

	if (connect(sock, (struct sockaddr *)&addr, sizeof(addr)) != 0) {
		ret = errno;
		close(sock);
		sock = -1;
		errno = ret;
		return ret;
	}

	return 0;
}

int ib_acm_connect(char *dest)
{
	if (dest && *dest == '/')
		return ib_acm_connect_unix(dest);

	return ib_acm_connect_open(dest);
}

void ib_acm_disconnect(void)
{
	if (sock != -1) {
		shutdown(sock, SHUT_RDWR);
		close(sock);
		sock = -1;
	}
}

static int acm_format_resp(struct acm_msg *msg,
	struct ibv_path_data **paths, int *count, int print)
{
	struct ibv_path_data *path_data;
	char addr[ACM_MAX_ADDRESS];
	int i, addr_cnt;

	*count = 0;
	addr_cnt = (msg->hdr.length - ACM_MSG_HDR_LENGTH) /
		sizeof(struct acm_ep_addr_data);
	path_data = (struct ibv_path_data *)
		calloc(1, addr_cnt * sizeof(struct ibv_path_data));
	if (!path_data)
		return -1;

	for (i = 0; i < addr_cnt; i++) {
		switch (msg->resolve_data[i].type) {
		case ACM_EP_INFO_PATH:
			path_data[i].flags = msg->resolve_data[i].flags;
			path_data[i].path  = msg->resolve_data[i].info.path;
			(*count)++;
			break;
		default:
			if (!(msg->resolve_data[i].flags & ACM_EP_FLAG_SOURCE))
				goto err;

			switch (msg->resolve_data[i].type) {
			case ACM_EP_INFO_ADDRESS_IP:
				inet_ntop(AF_INET, msg->resolve_data[i].info.addr,
					addr, sizeof addr);
				break;
			case ACM_EP_INFO_ADDRESS_IP6:
				inet_ntop(AF_INET6, msg->resolve_data[i].info.addr,
					addr, sizeof addr);
				break;
			case ACM_EP_INFO_NAME:
				memcpy(addr, msg->resolve_data[i].info.name,
					ACM_MAX_ADDRESS);
				break;
			default:
				goto err;
			}
			if (print)
				printf("Source: %s\n", addr);
			break;
		}
	}

	*paths = path_data;
	return 0;
err:
	free(path_data);
	return -1;
}

static int acm_format_ep_addr(struct acm_ep_addr_data *data, uint8_t *addr,
	uint8_t type, uint32_t flags)
{
	data->type   = type;
	data->flags  = flags;

	switch (type) {
	case ACM_EP_INFO_NAME:
		if (!check_snprintf((char *)data->info.name,
				   sizeof(data->info.name), "%s", (char *)addr))
			return -1;
		break;
	case ACM_EP_INFO_ADDRESS_IP:
		memcpy(data->info.addr, &((struct sockaddr_in *) addr)->sin_addr, 4);
		break;
	case ACM_EP_INFO_ADDRESS_IP6:
		memcpy(data->info.addr, &((struct sockaddr_in6 *) addr)->sin6_addr, 16);
		break;
	default:
		return -1;
	}

	return 0;
}

static inline int ERR(int err)
{
	errno = err;
	return -1;
}

static int acm_error(uint8_t status)
{
	switch (status) {
	case ACM_STATUS_SUCCESS:
		return 0;
	case ACM_STATUS_ENOMEM:
		return ERR(ENOMEM);
	case ACM_STATUS_EINVAL:
		return ERR(EINVAL);
	case ACM_STATUS_ENODATA:
		return ERR(ENODATA);
	case ACM_STATUS_ENOTCONN:
		return ERR(ENOTCONN);
	case ACM_STATUS_ETIMEDOUT:
		return ERR(ETIMEDOUT);
	case ACM_STATUS_ESRCADDR:
	case ACM_STATUS_EDESTADDR:
		return ERR(EADDRNOTAVAIL);
	case ACM_STATUS_ESRCTYPE:
	case ACM_STATUS_EDESTTYPE:
	default:
		return ERR(EINVAL);
	}
}

static int acm_resolve(uint8_t *src, uint8_t *dest, uint8_t type,
	struct ibv_path_data **paths, int *count, uint32_t flags, int print)
{
	struct acm_msg msg;
	int ret, cnt = 0;

	pthread_mutex_lock(&acm_lock);
	memset(&msg, 0, sizeof msg);
	msg.hdr.version = ACM_VERSION;
	msg.hdr.opcode = ACM_OP_RESOLVE;

	if (src) {
		ret = acm_format_ep_addr(&msg.resolve_data[cnt++], src, type,
			ACM_EP_FLAG_SOURCE);
		if (ret)
			goto out;
	}

	ret = acm_format_ep_addr(&msg.resolve_data[cnt++], dest, type,
		ACM_EP_FLAG_DEST | flags);
	if (ret)
		goto out;

	msg.hdr.length = ACM_MSG_HDR_LENGTH + (cnt * ACM_MSG_EP_LENGTH);

	ret = send(sock, (char *) &msg, msg.hdr.length, 0);
	if (ret != msg.hdr.length)
		goto out;

	ret = recv(sock, (char *) &msg, sizeof msg, 0);
	if (ret < ACM_MSG_HDR_LENGTH || ret != msg.hdr.length)
		goto out;

	if (msg.hdr.status) {
		ret = acm_error(msg.hdr.status);
		goto out;
	}

	ret = acm_format_resp(&msg, paths, count, print);
out:
	pthread_mutex_unlock(&acm_lock);
	return ret;
}

int ib_acm_resolve_name(char *src, char *dest,
	struct ibv_path_data **paths, int *count, uint32_t flags, int print)
{
	return acm_resolve((uint8_t *) src, (uint8_t *) dest,
		ACM_EP_INFO_NAME, paths, count, flags, print);
}

int ib_acm_resolve_ip(struct sockaddr *src, struct sockaddr *dest,
	struct ibv_path_data **paths, int *count, uint32_t flags, int print)
{
	if (((struct sockaddr *) dest)->sa_family == AF_INET) {
		return acm_resolve((uint8_t *) src, (uint8_t *) dest,
			ACM_EP_INFO_ADDRESS_IP, paths, count, flags, print);
	} else {
		return acm_resolve((uint8_t *) src, (uint8_t *) dest,
			ACM_EP_INFO_ADDRESS_IP6, paths, count, flags, print);
	}
}

int ib_acm_resolve_path(struct ibv_path_record *path, uint32_t flags)
{
	struct acm_msg msg;
	struct acm_ep_addr_data *data;
	int ret;

	pthread_mutex_lock(&acm_lock);
	memset(&msg, 0, sizeof msg);
	msg.hdr.version = ACM_VERSION;
	msg.hdr.opcode = ACM_OP_RESOLVE;
	msg.hdr.length = ACM_MSG_HDR_LENGTH + ACM_MSG_EP_LENGTH;

	data = &msg.resolve_data[0];
	data->flags = flags;
	data->type = ACM_EP_INFO_PATH;
	data->info.path = *path;

	ret = send(sock, (char *) &msg, msg.hdr.length, 0);
	if (ret != msg.hdr.length)
		goto out;

	ret = recv(sock, (char *) &msg, sizeof msg, 0);
	if (ret < ACM_MSG_HDR_LENGTH || ret != msg.hdr.length)
		goto out;

	ret = acm_error(msg.hdr.status);
	if (!ret)
		*path = data->info.path;

out:
	pthread_mutex_unlock(&acm_lock);
	return ret;
}

int ib_acm_query_perf(int index, uint64_t **counters, int *count)
{
	struct acm_msg msg;
	int ret, i;

	pthread_mutex_lock(&acm_lock);
	memset(&msg, 0, sizeof msg);
	msg.hdr.version = ACM_VERSION;
	msg.hdr.opcode = ACM_OP_PERF_QUERY;
	msg.hdr.src_index = index;
	msg.hdr.length = htobe16(ACM_MSG_HDR_LENGTH);

	ret = send(sock, (char *) &msg, ACM_MSG_HDR_LENGTH, 0);
	if (ret != ACM_MSG_HDR_LENGTH)
		goto out;

	ret = recv(sock, (char *) &msg, sizeof msg, 0);
	if (ret < ACM_MSG_HDR_LENGTH || ret != be16toh(msg.hdr.length)) {
		ret = ACM_STATUS_EINVAL;
		goto out;
	}

	if (msg.hdr.status) {
		ret = acm_error(msg.hdr.status);
		goto out;
	}

	*counters = malloc(sizeof(uint64_t) * msg.hdr.src_out);
	if (!*counters) {
		ret = ACM_STATUS_ENOMEM;
		goto out;
	}

	*count = msg.hdr.src_out;
	for (i = 0; i < *count; i++)
		(*counters)[i] = be64toh(msg.perf_data[i]);
	ret = 0;
out:
	pthread_mutex_unlock(&acm_lock);
	return ret;
}

int ib_acm_enum_ep(int index, struct acm_ep_config_data **data, uint8_t port)
{
	struct acm_ep_config_data *netw_edata = NULL;
	struct acm_ep_config_data *host_edata = NULL;
	struct acm_hdr hdr;
	struct acm_msg msg;
	int ret;
	int len;
	int i;

	pthread_mutex_lock(&acm_lock);
	memset(&msg, 0, sizeof msg);
	msg.hdr.version = ACM_VERSION;
	msg.hdr.opcode = ACM_OP_EP_QUERY;
	msg.hdr.src_out = index;
	msg.hdr.src_index = port;
	msg.hdr.length = htobe16(ACM_MSG_HDR_LENGTH);

	ret = send(sock, (char *) &msg, ACM_MSG_HDR_LENGTH, 0);
	if (ret != ACM_MSG_HDR_LENGTH)
		goto out;

	ret = recv(sock, (char *) &hdr, sizeof(hdr), 0);
	if (ret != sizeof(hdr)) {
		ret = ACM_STATUS_EINVAL;
		goto out;
	}

	if (hdr.status) {
		ret = acm_error(hdr.status);
		goto out;
	}

	len = be16toh(hdr.length) - sizeof(hdr);
	netw_edata = malloc(len);
	host_edata = malloc(len);
	if (!netw_edata || !host_edata) {
		ret = ACM_STATUS_ENOMEM;
		goto out;
	}

	ret = recv(sock, (char *)netw_edata, len, 0);
	if (ret != len) {
		ret = ACM_STATUS_EINVAL;
		goto out;
	}

	host_edata->dev_guid = be64toh(netw_edata->dev_guid);
	host_edata->port_num = netw_edata->port_num;
	host_edata->phys_port_cnt = netw_edata->phys_port_cnt;
	host_edata->pkey = be16toh(netw_edata->pkey);
	host_edata->addr_cnt = be16toh(netw_edata->addr_cnt);

	memcpy(host_edata->prov_name, netw_edata->prov_name,
	       sizeof(host_edata->prov_name));

	for (i = 0; i < host_edata->addr_cnt; ++i)
		host_edata->addrs[i] = netw_edata->addrs[i];

	*data = host_edata;
	ret = 0;
out:
	free(netw_edata);
	if (ret)
		free(host_edata);
	pthread_mutex_unlock(&acm_lock);
	return ret;
}

int ib_acm_query_perf_ep_addr(uint8_t *src, uint8_t type,
			     uint64_t **counters, int *count)
{
	struct acm_msg msg;
	int ret, i, len;

	if (!src)
		return -1;

	pthread_mutex_lock(&acm_lock);
	memset(&msg, 0, sizeof msg);
	msg.hdr.version = ACM_VERSION;
	msg.hdr.opcode = ACM_OP_PERF_QUERY;

	ret = acm_format_ep_addr(&msg.resolve_data[0], src, type,
		ACM_EP_FLAG_SOURCE);
	if (ret)
		goto out;

	len = ACM_MSG_HDR_LENGTH + ACM_MSG_EP_LENGTH;
	msg.hdr.length = htobe16(len);

	ret = send(sock, (char *) &msg, len, 0);
	if (ret != len)
		goto out;

	ret = recv(sock, (char *) &msg, sizeof msg, 0);
	if (ret < ACM_MSG_HDR_LENGTH || ret != be16toh(msg.hdr.length)) {
		ret = ACM_STATUS_EINVAL;
		goto out;
	}

	if (msg.hdr.status) {
		ret = acm_error(msg.hdr.status);
		goto out;
	}

	*counters = malloc(sizeof(uint64_t) * msg.hdr.src_out);
	if (!*counters) {
		ret = ACM_STATUS_ENOMEM;
		goto out;
	}

	*count = msg.hdr.src_out;
	for (i = 0; i < *count; i++)
		(*counters)[i] = be64toh(msg.perf_data[i]);

	ret = 0;
out:
	pthread_mutex_unlock(&acm_lock);
	return ret;
}


const char *ib_acm_cntr_name(int index)
{
	static const char *const cntr_name[] = {
		[ACM_CNTR_ERROR]	= "Error Count",
		[ACM_CNTR_RESOLVE]	= "Resolve Count",
		[ACM_CNTR_NODATA]	= "No Data",
		[ACM_CNTR_ADDR_QUERY]	= "Addr Query Count",
		[ACM_CNTR_ADDR_CACHE]	= "Addr Cache Count",
		[ACM_CNTR_ROUTE_QUERY]	= "Route Query Count",
		[ACM_CNTR_ROUTE_CACHE]	= "Route Cache Count",
	};

	if (index < ACM_CNTR_ERROR || index > ACM_MAX_COUNTER)
		return "Unknown";

	return cntr_name[index];
}
