/* -*- mode: c; c-file-style: "openbsd" -*- */
/*
 * Copyright (c) 2012 Vincent Bernat <bernat@luffy.cx>
 *
 * Permission to use, copy, modify, and/or distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 */

#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <poll.h>

#include "lldpctl.h"
#include "atom.h"
#include "../compat/compat.h"
#include "../ctl.h"
#include "../log.h"

const char *
lldpctl_get_default_transport(void)
{
	return LLDPD_CTL_SOCKET;
}

/* Connect to the remote end */
static int
sync_connect(lldpctl_conn_t *lldpctl, struct lldpctl_conn_sync_t *conn)
{
	return ctl_connect(lldpctl->ctlname);
}

/* Synchronously send data to remote end. */
static ssize_t
sync_send(lldpctl_conn_t *lldpctl, const uint8_t *data, size_t length, void *user_data)
{
	struct lldpctl_conn_sync_t *conn = user_data;
	ssize_t nb;

	if (conn->fd == -1 && ((conn->fd = sync_connect(lldpctl, conn)) == -1)) {
		return LLDPCTL_ERR_CANNOT_CONNECT;
	}

	while ((nb = write(conn->fd, data, length)) == -1) {
		if (errno == EAGAIN || errno == EINTR) continue;
		return LLDPCTL_ERR_CALLBACK_FAILURE;
	}
	return nb;
}

/* Statically receive data from remote end. */
static ssize_t
sync_recv(lldpctl_conn_t *lldpctl, const uint8_t *data, size_t length, void *user_data)
{
	struct lldpctl_conn_sync_t *conn = user_data;
	ssize_t nb = 0;
	size_t remain, offset = 0;

	if (conn->fd == -1 && ((conn->fd = sync_connect(lldpctl, conn)) == -1)) {
		lldpctl->error = LLDPCTL_ERR_CANNOT_CONNECT;
		return LLDPCTL_ERR_CANNOT_CONNECT;
	}

	struct pollfd fds[2];
	fds[0].fd = conn->pipe_fd[0];
	fds[0].events = POLLIN;
	fds[1].fd = conn->fd;
	fds[1].events = POLLIN;

	remain = length;
	do {
		if (poll(fds, sizeof(fds) / sizeof(fds[0]), -1) == -1) {
			if (errno == EINTR) continue;
			return LLDPCTL_ERR_CALLBACK_FAILURE;
		}

		if (fds[0].revents & POLLIN) {
			/* Unblock request received. */
			return LLDPCTL_ERR_CALLBACK_UNBLOCK;
		}

		if (fds[1].revents & POLLIN) {
			/* Message from daemon. */
			if ((nb = read(conn->fd, (unsigned char *)data + offset,
				 remain)) == -1) {
				if (errno == EAGAIN || errno == EINTR) continue;
				return LLDPCTL_ERR_CALLBACK_FAILURE;
			}
			remain -= nb;
			offset += nb;
		}
	} while (remain > 0 && nb != 0);
	return offset;
}

lldpctl_conn_t *
lldpctl_new(lldpctl_send_callback send, lldpctl_recv_callback recv, void *user_data)
{
	return lldpctl_new_name(lldpctl_get_default_transport(), send, recv, user_data);
}

lldpctl_conn_t *
lldpctl_new_name(const char *ctlname, lldpctl_send_callback send,
    lldpctl_recv_callback recv, void *user_data)
{
	int rc = LLDPCTL_ERR_FATAL;
	lldpctl_conn_t *conn = NULL;
	struct lldpctl_conn_sync_t *data = NULL;

	/* Both callbacks are mandatory or should be NULL. */
	if (send && !recv) return NULL;
	if (recv && !send) return NULL;

	if ((conn = calloc(1, sizeof(lldpctl_conn_t))) == NULL) return NULL;

	conn->ctlname = strdup(ctlname);
	if (conn->ctlname == NULL) {
		goto end;
	}
	if (!send && !recv) {
		if ((data = malloc(sizeof(struct lldpctl_conn_sync_t))) == NULL)
			goto end;
		if (pipe(data->pipe_fd) == -1) goto end;
		data->fd = -1;
		conn->send = sync_send;
		conn->recv = sync_recv;
		conn->user_data = data;
		conn->sync_clb = 1;
	} else {
		conn->send = send;
		conn->recv = recv;
		conn->user_data = user_data;
		conn->sync_clb = 0;
	}

	rc = LLDPCTL_NO_ERROR;

end:

	if (rc != LLDPCTL_NO_ERROR) {

		if (data) {
			free(data);
		}
		if (conn->ctlname) free(conn->ctlname);
		free(conn);
		conn = NULL;
	}

	return conn;
}

int
lldpctl_release(lldpctl_conn_t *conn)
{
	if (conn == NULL) return 0;
	free(conn->ctlname);
	if (conn->send == sync_send) {
		struct lldpctl_conn_sync_t *data = conn->user_data;
		close(data->pipe_fd[0]);
		close(data->pipe_fd[1]);
		if (data->fd != -1) close(data->fd);
		free(data);
	}
	free(conn->input_buffer);
	free(conn->output_buffer);
	free(conn);
	return 0;
}

/**
 * Request some bytes if they are not already here.
 *
 * @param conn   The connection to lldpd.
 * @param length The number of requested bytes.
 * @return A negative integer if we can't have the bytes or the number of bytes we got.
 */
ssize_t
_lldpctl_needs(lldpctl_conn_t *conn, size_t length)
{
	uint8_t *buffer;
	ssize_t rc;

	if ((buffer = calloc(1, length)) == NULL)
		return SET_ERROR(conn, LLDPCTL_ERR_NOMEM);
	rc = conn->recv(conn, buffer, length, conn->user_data);
	if (rc < 0) {
		free(buffer);
		return SET_ERROR(conn, rc);
	}
	if (rc == 0) {
		free(buffer);
		return SET_ERROR(conn, LLDPCTL_ERR_EOF);
	}
	rc = lldpctl_recv(conn, buffer, rc);
	free(buffer);
	if (rc < 0) return SET_ERROR(conn, rc);
	RESET_ERROR(conn);
	return rc;
}

static int
check_for_notification(lldpctl_conn_t *conn)
{
	struct lldpd_neighbor_change *change;
	void *p;
	int rc;
	lldpctl_change_t type;
	lldpctl_atom_t *interface = NULL, *neighbor = NULL;
	rc = ctl_msg_recv_unserialized(&conn->input_buffer, &conn->input_buffer_len,
	    NOTIFICATION, &p, &MARSHAL_INFO(lldpd_neighbor_change));
	if (rc != 0) return rc;
	change = p;

	/* We have a notification, call the callback */
	if (conn->watch_cb || conn->watch_cb2) {
		switch (change->state) {
		case NEIGHBOR_CHANGE_DELETED:
			type = lldpctl_c_deleted;
			break;
		case NEIGHBOR_CHANGE_ADDED:
			type = lldpctl_c_added;
			break;
		case NEIGHBOR_CHANGE_UPDATED:
			type = lldpctl_c_updated;
			break;
		default:
			log_warnx("control", "unknown notification type (%d)",
			    change->state);
			goto end;
		}
		interface = _lldpctl_new_atom(conn, atom_interface, change->ifname);
		if (interface == NULL) goto end;
		neighbor =
		    _lldpctl_new_atom(conn, atom_port, 0, NULL, change->neighbor, NULL);
		if (neighbor == NULL) goto end;
		if (conn->watch_cb)
			conn->watch_cb(conn, type, interface, neighbor,
			    conn->watch_data);
		else
			conn->watch_cb2(type, interface, neighbor, conn->watch_data);
		conn->watch_triggered = 1;
		goto end;
	}

end:
	if (interface) lldpctl_atom_dec_ref(interface);
	if (neighbor)
		lldpctl_atom_dec_ref(neighbor);
	else {
		lldpd_chassis_cleanup(change->neighbor->p_chassis, 1);
		lldpd_port_cleanup(change->neighbor, 1);
		free(change->neighbor);
	}
	free(change->ifname);
	free(change);

	/* Indicate if more data remains in the buffer for processing */
	return (rc);
}

ssize_t
lldpctl_recv(lldpctl_conn_t *conn, const uint8_t *data, size_t length)
{

	RESET_ERROR(conn);

	if (length == 0) return 0;

	/* Received data should be appended to the input buffer. */
	if (conn->input_buffer == NULL) {
		conn->input_buffer_len = 0;
		if ((conn->input_buffer = malloc(length)) == NULL)
			return SET_ERROR(conn, LLDPCTL_ERR_NOMEM);
	} else {
		uint8_t *new =
		    realloc(conn->input_buffer, conn->input_buffer_len + length);
		if (new == NULL) return SET_ERROR(conn, LLDPCTL_ERR_NOMEM);
		conn->input_buffer = new;
	}
	memcpy(conn->input_buffer + conn->input_buffer_len, data, length);
	conn->input_buffer_len += length;

	/* Read all notifications */
	while (!check_for_notification(conn))
		;

	RESET_ERROR(conn);

	return conn->input_buffer_len;
}

int
lldpctl_process_conn_buffer(lldpctl_conn_t *conn)
{
	int rc;

	rc = check_for_notification(conn);

	RESET_ERROR(conn);

	return rc;
}

ssize_t
lldpctl_send(lldpctl_conn_t *conn)
{
	/* Send waiting data. */
	ssize_t rc;

	RESET_ERROR(conn);

	if (!conn->output_buffer) return 0;
	rc = conn->send(conn, conn->output_buffer, conn->output_buffer_len,
	    conn->user_data);
	if (rc < 0) return SET_ERROR(conn, rc);

	/* "Shrink" the output buffer. */
	if (rc == conn->output_buffer_len) {
		free(conn->output_buffer);
		conn->output_buffer = NULL;
		conn->output_buffer_len = 0;
		RESET_ERROR(conn);
		return rc;
	}
	conn->output_buffer_len -= rc;
	memmove(conn->output_buffer, conn->output_buffer + rc, conn->output_buffer_len);
	/* We don't shrink the buffer. It will be either freed or shrinked later */
	RESET_ERROR(conn);
	return rc;
}
