/*
 * Mini-ipc: Exported functions for server operation
 *
 * Copyright (C) 2011 CERN (www.cern.ch)
 * Author: Alessandro Rubini <rubini@gnudd.com>
 * Based on ideas and code by Tomasz Wlostowski <tomasz.wlostowski@cern.ch>
 *
 *   This program is free software; you can redistribute it and/or modify
 *   it under the terms of the GNU General Public License as published by
 *   the Free Software Foundation; either version 2 of the License, or
 *   (at your option) any later version.
 */
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <sys/select.h>
#include <time.h>

#include "minipc-int.h"

/*
 * This function creates a server structure and links it to the
 * process-wide list of links
 */
struct minipc_ch *minipc_server_create(const char *name, int f)
{
	return __minipc_link_create(name, MPC_USER_FLAGS(f) | MPC_FLAG_SERVER);
}

/*
 * The following ones add to the export list and remove from it
 */
int minipc_export(struct minipc_ch *ch, const struct minipc_pd *pd)
{
	struct mpc_link *link = mpc_get_link(ch);
	struct mpc_flist *flist;

	CHECK_LINK(link);

	flist = calloc(1, sizeof(*flist));
	if (!flist)
		return -1;
	flist->pd = pd;
	flist->next = link->flist;
	link->flist = flist;
	if (link->logf)
		fprintf(link->logf, "%s: exported %p (%s) with pd %p --"
			" retval %08x, args %08x...\n", __func__,
			flist, pd->name, pd, pd->retval, pd->args[0]);
	return 0;
}

int minipc_unexport(struct minipc_ch *ch, const struct minipc_pd *pd)
{
	struct mpc_link *link = mpc_get_link(ch);
	struct mpc_flist *flist;

	CHECK_LINK(link);
	/* We must find the flist that points to pd */
	for (flist = link->flist; flist; flist = flist->next)
		if (flist->pd == pd)
			break;
	if (!flist) {
		if (link->logf)
			fprintf(link->logf, "%s: not found pd %p\n",
					   __func__, pd);
		errno = EINVAL;
		return -1;
	}
	flist = container_of(&pd, struct mpc_flist, pd);
	mpc_free_flist(link, flist);
	return 0;
}

/* Return the current fdset associated to the service */
int minipc_server_get_fdset(struct minipc_ch *ch, fd_set *setptr)
{
	struct mpc_link *link = mpc_get_link(ch);

	CHECK_LINK(link);
	*setptr = link->fdset;
	return 0;
}

/* Get a pointer to the next argument in the array */
uint32_t *minipc_get_next_arg(uint32_t arg[], uint32_t atype)
{
	int asize;
	char *s = (void *)arg;

	if (MINIPC_GET_ATYPE(atype) != MINIPC_ATYPE_STRING)
		asize = MINIPC_GET_ANUM(MINIPC_GET_ASIZE(atype));
	else
		asize = MINIPC_GET_ANUM(strlen(s) + 1);
	return arg + asize;
}


/*
 * Internal functions used by server action below: handle a request
 * or the arrival of a new client
 */
static void mpc_handle_client(struct minipc_ch *ch, int pos, int fd)
{
	struct mpc_link *link = mpc_get_link(ch);
	struct mpc_req_packet *p_in, _pkt_in;
	struct mpc_rep_packet *p_out, _pkt_out;
	struct mpc_shmem *shm = link->memaddr;
	const struct minipc_pd *pd;
	struct mpc_flist *flist;
	int i;

	if (shm) {
		p_in = &shm->request;
		p_out = &shm->reply;
		/* read one byte, it's just a signal */
		mpc_mem_request( ch, -1 );
	} else {
		p_in = & _pkt_in;
		p_out = & _pkt_out;
		/* receive the packet and manage errors */
		i = recv(fd, p_in, sizeof(*p_in), 0);
		if (i < 0 && errno == EINTR)
			return;
		if (i <= 0)
			goto close_client;
	}

	/* use p_in->name to look for the function */
	for (flist = link->flist; flist; flist = flist->next)
		if (!(strcmp(p_in->name, flist->pd->name)))
			break;
	if (!flist) {
		if (link->logf)
			fprintf(link->logf, "%s: function %s not found\n",
				__func__, p_in->name);
		p_out->type = MINIPC_ARG_ENCODE(MINIPC_ATYPE_ERROR, int);
		*(int *)(&p_out->val) = EOPNOTSUPP;
		goto send_reply;
	}
	pd = flist->pd;
	if (link->logf)
		fprintf(link->logf, "%s: request for %s\n",
			__func__, pd->name);

	/* call the function and send back stuff */
	i = pd->f(pd, p_in->args, p_out->val);
	if (i < 0) {
		p_out->type = MINIPC_ARG_ENCODE(MINIPC_ATYPE_ERROR, int);
		*(int *)(&p_out->val) = errno;
	} else {
		/* Use retval, but fix the length for strings */
		if (MINIPC_GET_ATYPE(pd->retval) == MINIPC_ATYPE_STRING) {
			int size = strlen((char *)p_out->val) + 1;

			size = (size + 3) & ~3; /* align */
			p_out->type =
				__MINIPC_ARG_ENCODE(MINIPC_ATYPE_STRING, size);
		} else {
			p_out->type = pd->retval;
		}
	}

 send_reply:
	if (shm) {
		shm->nreply++; /* message already in place */
		return;
	}
	/* send a 32-bit value plus the declared return length */
	if (send(fd, p_out, sizeof(p_out->type)
	     + MINIPC_GET_ASIZE(p_out->type), MSG_NOSIGNAL) < 0)
		goto close_client;
	return;

 close_client:
	if (link->logf)
		fprintf(link->logf, "%s: error %i in fd %i, closing\n",
			__func__, i < 0 ? errno : 0, fd);
	close(fd);
	FD_CLR(fd, &link->fdset);
	link->fd[pos] = -1;
	return;
}

static void mpc_handle_connection(struct mpc_link *link, int fd)
{
	int i, newfd;
	struct sockaddr_un sun;
	socklen_t slen = sizeof(sun);

	newfd = accept(fd, (struct sockaddr *)&sun, &slen);
	if (link->logf)
		fprintf(link->logf, "%s: accept returned fd %i (error %i)\n",
			__func__, newfd, newfd < 0 ? errno : 0);
	if (newfd < 0)
		return;
	/* Lookf for a place for this */
	for (i = 0; i < MINIPC_MAX_CLIENTS; i++)
		if (link->fd[i] < 0)
			break;
	if (i == MINIPC_MAX_CLIENTS) {
		if (link->logf)
			fprintf(link->logf, "%s: refused: too many clients\n",
				__func__);
		close(newfd);
		return;
	}
	link->fd[i] = newfd;
	FD_SET(newfd, &link->fdset);
}


/*
 * The server action returns an error or zero. If the user wants
 * the list of active descriptors, it must ask for the filemask
 * (at this point there is no support for poll, only select)
 */
int minipc_server_action(struct minipc_ch *ch, int timeoutms)
{
	struct mpc_link *link = mpc_get_link(ch);
	struct timeval to;
	fd_set set;
	int i;

	CHECK_LINK(link);

	if ( !link->memaddr ) { // socket mode
		to.tv_sec = timeoutms/1000;
		to.tv_usec = (timeoutms % 1000) * 1000;
		set = link->fdset;
		i = select(64 /* FIXME: hard constant */, &set, NULL, NULL, &to);
		if (!i)
			return 0;
		if (i < 0 && errno == EINTR)
			return 0;
		if (i < 0)
			return -1;
		if (i == 0) {
			errno = EAGAIN;
			return -1;
		}
	} else {
		mpc_handle_client(ch, -1, 0);
	}

	/* First, look for all clients */
	for (i = 0; i < MINIPC_MAX_CLIENTS; i++) {
		if (link->fd[i] < 0)
			continue;
		if (!FD_ISSET(link->fd[i], &set))
			continue;
		mpc_handle_client(ch, i, link->fd[i]);
	}
	/* Finally, look for a new client */
	if (FD_ISSET(ch->fd, &set))
		mpc_handle_connection(link, ch->fd);
	return 0;
}


#define AVERAGE_REQ_TIME_US 1000 /* 1ms busy wait per request */

/* get monotonic number of useconds */
static uint64_t get_monotonic_us(void)
{
	struct timespec tv;
	clock_gettime(CLOCK_MONOTONIC, &tv);
 
	return (uint64_t) tv.tv_sec * 1000000ULL +
			(uint64_t) (tv.tv_nsec / 1000);
}


int mpc_mem_request(struct minipc_ch *ch, int timeout_ms)
{
	uint64_t start_tics = get_monotonic_us();
	uint64_t timeout_tics = start_tics + 1000ULL * timeout_ms;
	uint64_t busy_tics = start_tics + AVERAGE_REQ_TIME_US;

	uint32_t prev;
	volatile uint32_t *vptr;
	struct mpc_link *link = mpc_get_link(ch);
	volatile struct mpc_shmem *shm = link->memaddr;

	if (link->flags & MPC_FLAG_SERVER)
	{
		vptr = &shm->nrequest;
		prev = link->last_request_id;
	}
	else
	{
		vptr = &shm->nreply;
		prev = *vptr;
		shm->nrequest++;
	}

	for(;;)
	{
		uint64_t t = get_monotonic_us();

		if( *vptr != prev )
			return 0;

		if( t > timeout_tics && timeout_ms > 0 ) {
			errno = ETIMEDOUT;
			return -1;
		} else if ( t > busy_tics ) {
			usleep( MINIPC_DEFAULT_POLL );
		}
	}

	return 0;
}